1#!/usr/bin/ruby 2require 'fileutils' 3 4Encoding.default_external = Encoding::UTF_8 5 6def scangrp(inf) 7 line = "" 8 res = {} 9 group = "unknown" 10 11 while ( (line = inf.readline)[0..1] == "--") do 12# special paragraph break 13 if line[0..2] == "--\n" 14 res[group] << "" 15 16# switch group 17 elsif line[0..3] == "-- @" 18 group = line[4..line.index(":")-1] 19 msg = line[(line.index(":")+1)..-1] 20 if (res[group] == nil) then 21 res[group] = [] 22 else 23 res[group] << "" 24 end 25 26# some groups have a header 27 res[group] << msg.strip if msg != nil 28# more data 29 else 30 res[group] << line[3..-1].strip 31 end 32 end 33 34 return res, line 35 36rescue => er 37 STDERR.print("parsing error, #{er} while processing " \ 38 "(#{inf.path}\n lastline: #{line}\n" 39 ) 40 return res, "" 41end 42 43class String 44 def join(a) 45 "#{self}#{a}" 46 end 47end 48 49# run through the list of lines through the C pre-parser, this is used multiple 50# times with MAIN, MAIN2, ERROR1, ... defined in order to both generate test 51# cases and for manpage examples 52def extract_example(lines, defs) 53 res = [] 54 iobj = IO.popen( 55 "cpp -pipe -fno-builtin -fno-common #{defs}", 56 File::RDWR 57 ) 58 iobj.print(lines) 59 iobj.close_write 60 iobj.each_line{|a| 61 if (a[0] == "#" or a.strip == "") 62 next 63 end 64 line = a.gsub(/\n/, "") 65 line = (" " * line[/\A */].size) << line 66 res << line; 67 } 68 iobj.close_read 69 res 70end 71 72def extract_examples(name, inlines) 73 res_ok = [] 74 res_fail = [] 75 76 base_sz = extract_example(inlines, "").size 77 78 10.times{|a| 79 suff = a == 0 ? "" : (a + 1).to_s 80 lines = extract_example(inlines, "-DMAIN#{suff} -Dmain=#{name}#{a}") 81 if (lines.size == base_sz) 82 break 83 else 84 res_ok << lines.join("\n") 85 end 86 } 87 88 10.times{|a| 89 suff = a == 0 ? "" : (a + 1).to_s 90 lines = extract_example(inlines, "-DERROR#{suff} -Dmain=#{name}#{a}") 91 if (lines.size == base_sz) 92 break 93 else 94 res_fail << lines.join("\n") 95 end 96 } 97 98 return res_ok, res_fail 99end 100 101# 102# Missing; 103# alias, flags 104# 105class DocReader 106 def initialize 107 @short = "" 108 @inargs = [] 109 @outargs = [] 110 @longdescr = "" 111 @group = "" 112 @cfunction = "" 113 @error = [] 114 @related = [] 115 @note = [] 116 @examples = [ [], [] ] 117 @main_tests = [] 118 @error_tests = [] 119 end 120 121 def incomplete? 122 return @short.length == 0 123 end 124 125 def DocReader.Open(fname) 126 typetbl = { 127 "int" => true, 128 "inttbl" => true, 129 "bool" => true, 130 "float" => true, 131 "floattbl" => true, 132 "str" => true, 133 "strtbl" => true, 134 "vid" => true, 135 "vidtbl" => true, 136 "aidtbl" => true, 137 "func" => true, 138 } 139 a = File.open(fname) 140 if (a == nil) then 141 return 142 end 143 144 res = DocReader.new 145 res.name = a.readline[3..-1].strip 146 groups, line = scangrp(a) 147 groups.each_pair{|a, v| 148 if res.respond_to? a.to_sym 149 res.send("#{a}=".to_sym, v); 150 end 151 } 152 153 while (line.strip! == "") 154 begin 155 line = a.readline 156 rescue EOFError 157 return res 158 end 159 end 160 161 lines = a.readlines 162 lines.insert(0, line) 163 remainder = lines.join("\n") 164 165 ok, err = extract_examples(res.name, remainder) 166 res.examples[ 0 ] = ok 167 res.examples[ 1 ] = err 168 169# verify the inargs field to make sure that it is typed 170 res.inargs.delete_if{|a| a.length == 0} 171 res.outargs.delete_if{|a| a.length == 0} 172 res.inargs.each{|a| 173 largs = a.split(/,/) 174 largs.each{|b| 175 type = b.split(/:/) 176 if (not type or type.size != 2) 177 res.error << "missing type on #{b}" 178 next 179 end 180 res.error << "unknown type #{type[0]}" unless typetbl[type[0]] 181 } 182 } 183 184 res 185 186 rescue EOFError 187 res 188 rescue => er 189 p er 190 p er.backtrace 191 nil 192 end 193 194 def note=(v) 195 @note << v 196 end 197 198 def ok?() 199 return @error.length == 0 200 end 201 202 attr_accessor :short, :inargs, :outargs, 203 :longdescr, :group, :cfunction, :related, 204 :examples, :main_tests, :error_tests, :name, :error 205 206 attr_reader :note 207 208 private :initialize 209end 210 211def find_empty() 212 Dir["*.lua"].each{|a| 213 if ( DocReader.Open(a).incomplete? ) then 214 yield a 215 end 216 } 217 218 nil 219end 220 221def find_old() 222 Dir["*.lua"].each{|a| 223 doc = DocReader.Open(a) 224 unless (doc.ok? ) then 225 yield a, doc.error 226 end 227 } 228end 229 230def add_function(groupname, symname, cname) 231 fn = "#{symname}.lua" 232 if (File.exist?(fn)) 233 true 234# 235# should checksum C source function, compare to 236# whatever is stored in the .lua file header 237# and alter the user if the function has changed 238# (i.e. documentation might need to be updated) 239 # 240 else 241 STDOUT.print("--- new function found: #{symname}\n") 242 outf = File.new(fn, IO::CREAT | IO::RDWR) 243 outf.print("-- #{symname}\n\ 244-- @short: \n\ 245-- @inargs: \n\ 246-- @outargs: \n\ 247-- @longdescr: \n\ 248-- @group: #{groupname} \n\ 249-- @cfunction: #{cname}\n\ 250-- @related:\n\ 251\ 252function main() 253\#ifdef MAIN 254\#endif 255 256\#ifdef ERROR1 257\#endif 258end\n") 259 end 260end 261 262# 263# Parse the C binding file. look for our preprocessor 264# markers, extract the lua symbol, c symbol etc. and 265# push to the function pointer in cfun 266# 267def cscan(cfun, cfile) 268 in_grp = false 269 linec = 0 270 File.open(cfile).each_line{|line| 271 linec = linec + 1 272 if (not in_grp and line =~ /\#define\sEXT_MAPTBL_(\w+)/) 273 in_grp = $1 274 elsif (in_grp) 275 if (line =~ /\#undef\sEXT_MAPTBL_(\w+)/) 276 in_grp = nil 277 else 278 line =~ /\{\"([a-z0-9_]+)\",\s*([a-z0-9_]+)\s*\}/ 279 if ($1 != nil and $2 != nil) then 280 send(cfun, in_grp.downcase, $1, $2) 281 end 282 end 283 end 284 } 285rescue => er 286 STDOUT.print("exception at line #{linec}:#{er} in #{cfile}") 287end 288 289$grouptbl = {} 290def scangroups(group, sym, csym) 291 if ($grouptbl[group] == nil) then 292 $grouptbl[group] = [] 293 end 294 $grouptbl[group] << sym 295end 296 297# special paragraphing fixups for *ROFF output (manpages) in 298# the context @longdescr group 299# 300# empty -> \n.PP\n 301# *bla* -> \n.I bla\n 302# ref:function\w -> \n.BR function\n 303# . text -> \n.IP 304# \[A-Z_]1+ -> \n.B MSG\n 305# 306def troff_highlight_str(str) 307 str.gsub!(/([A-Z_]{2,})/, "\n.B \\1\n\\\\&") 308 str.gsub!(/\*([a-z_]{2,})\*/, "\n.I \\1\n\\\\&") 309 str.gsub!(/ref:(\w{2,})/, "\n.BR \\1 \n\\\\&") 310 str 311end 312 313def troffdescr(descr) 314 out = "" 315 descr.each{|block| 316 case block 317 when "" then out << "\n.PP\n" 318 when /^\.\s/ then out << troff_highlight_str("\n.B #{block[2..-1]} \n") 319 else 320 out << troff_highlight_str(block) << " " 321 end 322 } 323 324 out.strip 325end 326 327# template: 328# .TH PRJ 3 "Date" System "Group" 329# .SH NAME 330# function \- descr 331# .SH SYNOPSIS 332# .I retval 333# .B fname 334# .R ( 335# .I arg 336# .I arg2 337# .R ) 338# .SH DESCRIPTION 339# .B somethingsomething 340# somethingsoemthing 341# .BR highlight 342# something 343# .SH NOTES 344# .IP seqn. 345# descr 346# .SH EXAMPLE 347# if present 348# .SH SEE ALSO 349# 350 351def funtoman(fname, outm) 352 inf = DocReader.Open("#{fname}.lua") 353 outm << ".\\\" groff -man -Tascii #{fname}.3\n" 354 outm << ".TH \"#{fname}\" 3 \"#{Time.now.strftime( 355 "%B %Y")}\" #{inf.group[0]} \"Arcan Lua API\"\n" 356 outm << ".SH NAME\n" 357 outm << ".B #{fname} \ - \n#{inf.short.join(" ")}\n" 358 outm << ".SH SYNOPSIS\n" 359 360 if (inf.outargs.size > 0) 361 outm << ".I #{inf.outargs.join(", ")}\n" 362 else 363 outm << ".I nil \n" 364 end 365 366 if (inf.inargs.size == 0) 367 outm << ".br\n.B #{fname}()\n" 368 else 369 inf.inargs.each{|argf| 370 outm << ".br\n.B #{fname}(\n" 371 tbl = argf.split(/\,/) 372 tbl.each_with_index{|a, b| 373 if (a =~ /\*/) 374 outm << "\n.I #{a.gsub(/\*/, "").gsub(" ", " ").strip}" 375 else 376 outm << "#{a.strip}" 377 end 378 379 if (b < tbl.size - 1) then 380 outm << ", " 381 else 382 outm << "\n" 383 end 384 } 385 outm << ")\n" 386 } 387 end 388 389 if (inf.longdescr.size > 0) 390 outm << ".SH DESCRIPTION\n" 391 outm << troffdescr(inf.longdescr) 392 outm << "\n\n" 393 end 394 395 if (inf.note.size > 0) 396 outm << ".SH NOTES\n.IP 1\n" 397 count = 1 398 399 inf.note[0].each{|a| 400 if (a.strip == "") 401 count = count + 1 402 outm << ".IP #{count}\n" 403 else 404 outm << "#{troff_highlight_str(a)}\n" 405 end 406 } 407 end 408 409 inf.examples[0].each{|a| 410 outm << ".SH EXAMPLE\n.nf \n\n" 411 outm << a 412 outm << "\n.fi\n" 413 } 414 415 inf.examples[1].each{|a| 416 outm << ".SH MISUSE\n.nf \n\n" 417 outm << a 418 outm << "\n.fi\n" 419 } 420 421 if (inf.related.size > 0) 422 outm << ".SH SEE ALSO:\n" 423 inf.related[0].split(",").each{|a| 424 outm << ".BR #{a.strip} (3)\n" 425 } 426 outm << "\n" 427 end 428 429rescue => er 430 STDERR.print("Failed to parse/generate (#{fname} reason: #{er}\n #{ 431 er.backtrace.join("\n")})\n") 432end 433 434case (ARGV[0]) 435when "scan" then 436 cf = ENV["ARCAN_SOURCE_DIR"] ? 437 "#{ENV["ARCAN_SOURCE_DIR"]}/engine/arcan_lua.c" : 438 "../src/engine/arcan_lua.c" 439 440 cscan(:add_function, cf) 441 442when "vimgen" then 443 444 kwlist = [] 445 446# could do something more with this, i.e. maintain group/category relations 447 Dir["*.lua"].each{|a| 448 a = DocReader.Open(a) 449 kwlist << a.name 450 } 451 452 fname = ARGV[1] 453 if fname == nil then 454 Dir["/usr/share/vim/vim*"].each{|a| 455 next unless Dir.exist?(a) 456 if File.exist?("#{a}/syntax/lua.vim") then 457 fname = "#{a}/syntax/lua.vim" 458 break 459 end 460 } 461 end 462 463 if (fname == nil) then 464 STDOUT.print("Couldn't find lua.vim, please specify on the command-line") 465 exit(1) 466 end 467 468 consts = [] 469 if (File.exist?("constdump/consts.list")) 470 File.open("constdump/consts.list").each_line{|a| 471 consts << a.chop 472 } 473 else 474 STDOUT.print("No constdump/consts.list found, constants ignored.\n\ 475run arcan with constdump folder to generate.\n") 476 end 477 478 lines = File.open(fname).readlines 479 File.delete("arcan-lua.vim") if File.exist?("arcan-lua.vim") 480 outf = File.new("arcan-lua.vim", IO::CREAT | IO::RDWR) 481 482 last_ch = "b" 483 lines[1..-5].each{|a| 484 if (a =~ /let\s(\w+):current_syntax/) then 485 last_ch = $1 486 next 487 end 488 outf.print(a) 489 } 490 491 kwlist.each{|a| 492 next if (a.chop.length == 0) 493 outf.print("syn keyword luaFunc #{a}\n") 494 } 495 496 consts.each{|a| 497 outf.print("syn keyword luaConstant #{a}\n") 498 } 499 500 outf.print("let #{last_ch}:current_syntax = \"arcan_lua\"\n") 501 502 lines[-4..-1].each{|a| outf.print(a) } 503 504when "testgen" then 505 if ARGV[1] == nil or Dir.exist?(ARGV[1]) == false or 506 ARGV[2] == nil or Dir.exist?(ARGV[2]) == false 507 STDOUT.print("input or output directory wrong or omitted\n") 508 exit(1) 509 end 510 511 indir = ARGV[1] 512 outdir = ARGV[2] 513 FileUtils.rm_r("#{outdir}/test_ok") if Dir.exists?("#{outdir}/test_ok") 514 FileUtils.rm_r("#{outdir}/test_fail") if Dir.exists?("#{outdir}/test_fail") 515 Dir.mkdir("#{outdir}/test_ok") 516 Dir.mkdir("#{outdir}/test_fail") 517 518 ok_count = 0 519 fail_count = 0 520 missing_ok = [] 521 missing_fail = [] 522 523 Dir["#{indir}/*.lua"].each{|a| 524 doc = DocReader.Open(a) 525 if (doc.examples[0].size > 0) 526 ok_count += doc.examples[0].size 527 doc.examples[0].size.times{|c| 528 Dir.mkdir("#{outdir}/test_ok/#{doc.name}#{c}") 529 outf = File.new("#{outdir}/test_ok/#{doc.name}#{c}/#{doc.name}#{c}.lua", 530 IO::CREAT | IO::RDWR) 531 outf.print(doc.examples[0][c]) 532 outf.close 533 } 534 else 535 missing_ok << a 536 end 537 538 if (doc.examples[1].size > 0) 539 fail_count += doc.examples[1].size 540 doc.examples[1].size.times{|c| 541 Dir.mkdir("#{outdir}/test_fail/#{doc.name}#{c}") 542 outf = File.new( 543 "#{outdir}/test_fail/#{doc.name}#{c}/#{doc.name}#{c}.lua", 544 IO::CREAT | IO::RDWR 545 ) 546 outf.print(doc.examples[0][c]) 547 outf.close 548 } 549 else 550 missing_fail << a 551 end 552 } 553 554 STDOUT.print("OK:#{ok_count},FAIL:#{fail_count}\n\ 555Missing OK:#{missing_ok.join(",")}\n\ 556Missing Fail:#{missing_fail.join(",")}\n") 557 558when "verify" then 559 find_empty(){|a| 560 STDOUT.print("#{a} is incomplete (missing test/examples).\n") 561 } 562 563 find_old(){|a, b| 564 STDOUT.print("#{a} dated/incorrect formatting: #{b}.\n") 565 } 566 567when "view" then 568 funtoman(ARGV[1], STDOUT) 569 570when "mangen" then 571 inf = File.open("arcan_api_overview_hdr") 572 573 if (Dir.exist?("mantmp")) 574 Dir["mantmp/*"].each{|a| File.delete(a) } 575 else 576 Dir.mkdir("mantmp") 577 end 578 579 outf = File.new("mantmp/arcan_api_overview.3", IO::CREAT | IO::RDWR) 580 inf.each_line{|line| outf << line} 581 582# populate $grptbl with the contents of the lua.c file 583 cf = ENV["ARCAN_SOURCE_DIR"] ? "#{ENV["ARCAN_SOURCE_DIR"]}/engine/arcan_lua.c" : 584 "../src/engine/arcan_lua.c" 585 586 cscan(:scangroups, cf) 587 588# add the functions of each group to a section in the 589# overview file 590 $grouptbl.each_pair{|key, val| 591 outf << ".SH #{key}\n" 592 val.each{|i| 593 outf << "\\&\\fI#{i}\\fR\\|(3)\n" 594 595 if File.exist?("#{i}.lua") 596 funtoman(i, File.new("mantmp/#{i}.3", IO::CREAT | IO::RDWR)) 597 end 598 } 599 } 600 inf = File.open("arcan_api_overview_ftr").each_line{|line| 601 outf << line; 602 } 603 604 outf.close 605 606else 607 STDOUT.print("Usage: ruby docgen.rb command\nscan:\n\ 608scrape arcan_lua.c and generate stubs for missing corresponding .lua\n\n\ 609mangen:\n sweep each .lua file and generate corresponding manpages.\n\n\ 610vimgen:\n generate a syntax highlight .vim file that takes the default\n\ 611vim lua syntax file and adds the engine functions as new built-in functions.\n\ 612\ntestgen indir outdir:\n extract MAIN, MAIN2, ... and ERROR1, ERROR2 etc. \ 613from each lua file in indir\n\ and add as individual tests in the \n\ 614outdir/test_ok\ outdir/test_fail\ subdirectories\n\n\ 615missing:\n scan all .lua files and list those that are incomplete.\n\n 616view:\n convert a single function to man-format and send to stdout.\n\n\ 617verify:\n scan all .lua files and list those that use the old format. or \ 618specify wrong/missing argument types.\n") 619end 620