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