00001 --[[--------------------------------------------------------------------------
00002 -- Copyright (C) 2012 by Simon Dales --
00003 -- simon@purrsoft.co.uk --
00004 -- --
00005 -- This program is free software; you can redistribute it and/or modify --
00006 -- it under the terms of the GNU General Public License as published by --
00007 -- the Free Software Foundation; either version 2 of the License, or --
00008 -- (at your option) any later version. --
00009 -- --
00010 -- This program is distributed in the hope that it will be useful, --
00011 -- but WITHOUT ANY WARRANTY; without even the implied warranty of --
00012 -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --
00013 -- GNU General Public License for more details. --
00014 -- --
00015 -- You should have received a copy of the GNU General Public License --
00016 -- along with this program; if not, write to the --
00017 -- Free Software Foundation, Inc., --
00018 -- 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. --
00019 ----------------------------------------------------------------------------]]
00020
00021 --[[!
00022 \file
00023 \brief a hack lua2dox converter
00024 ]]
00025
00026 --[[!
00027 \mainpage
00028
00029
00030 A hack lua2dox converter
00031 Version 0.1
00032
00033 This lets us make Doxygen output some documentation to let
00034 us develop this code.
00035
00036 It is partially cribbed from the functionality of lua2dox
00037 (http:
00038 Found on CPAN when looking for something else; kinda handy.
00039
00040 Improved from lua2dox to make the doxygen output more friendly.
00041 Also it runs faster in lua rather than Perl.
00042
00043 Because this Perl based system is called "lua2dox"., I have decided to add ".lua" to the name
00044 to keep the two separate.
00045
00046 0. Ensure doxygen is installed on your system and that you are familiar with its use.
00047 Best is to try to make and document some simple C/C++/PHP to see what it produces.
00048
00049 1. Run "lua2dox_lua -g" to create a default Doxyfile.
00050
00051 Then alter it to let it recognise lua. Add the two following lines:
00052
00053 FILE_PATTERNS = *.lua
00054
00055 FILTER_PATTERNS = *.lua=lua2dox_filter
00056
00057 Either add them to the end or find the appropriate entry in Doxyfile.
00058
00059 2. When Doxyfile is edited run as "lua2dox_lua"
00060
00061 When reading source with classes multiple passes are needed.
00062 Each pass generates a list of member functions (as a file) that were found on this pass.
00063 This list is read in on the next pass.
00064 If the class+methods haven't changed this time then you only need to run it once, else run twice.
00065
00066 The core function reads the input file (filename or stdin) and outputs some pseudo C-ish language.
00067 It only has to be good enough for doxygen to see it as legal.
00068 Therefore our lua interpreter is fairly limited, but "good enough".
00069
00070 One limitation is that each line is treated separately (except for long comments).
00071 The implication is that class and function declarations must be on the same line.
00072 Some functions can have their parameter lists extended over multiple lines to make it look neat.
00073 Managing this where there are also some comments is a bit more coding than I want to do at this stage,
00074 so it will probably not document accurately if we do do this.
00075
00076 However I have put in a hack that will insert the "missing" close paren.
00077 The effect is that you will get the function documented, but not with the parameter list you might expect.
00078
00079 Installation:
00080
00081 Here for linux or unix-like, for any other OS you need to refer to other documentation.
00082
00083 This file is "lua2dox.lua". It gets called by "lua2dox_lua".
00084 Somewhere in your path (e.g. "~/bin" or "/usr/local/bin") put two links to "lua2dox_lua".
00085 Names to use are "lua2dox_lua" and "lua2dox_filter".
00086
00087 Call it as "lua2dox_lua" and the filter that gets called by doxygen is "lua2dox_filter".
00088
00089 ]]
00090
00091 -- we won't use our library code, so this becomes more portable
00092
00093 local TConfig_config={
00094 ['LUA2DOX_COMMENTARY_FILE_IN']='./killme_lua2dox.methods'
00095 ,['LUA2DOX_COMMENTARY_FILE_OUT']='./killme_lua2dox.methods.out'
00096 }
00097
00098 --! \brief Gets a config val
00099 local function TConfig_get(Key,Default)
00100 local val = TConfig_config[Key]
00101 if not val then
00102 val = Default
00103 end
00104 return val
00105 end
00106
00107 --! \brief sets a config val
00108 local function TConfig_set(Key,Val)
00109 TConfig_config[Key] = Val
00110 end
00111
00112 --! \brief write to stdout
00113 --!
00114 --! writes Str (if not nil)
00115 local function TIO_write(Str)
00116 if (Str) then
00117 io.write(Str)
00118 end
00119 end
00120
00121 --! \brief write to stdout
00122 --!
00123 --! writelns Str (if not nil) and then an eoln.
00124 local function TIO_writeln(Str)
00125 if (Str) then
00126 io.write(Str)
00127 end
00128 io.write("\n")
00129 end
00130
00131 --! \brief show error to stdout
00132 --!
00133 --! writelns Str (if not nil) and then an eoln.
00134 local function TIO_showError(Err,Str)
00135 local err = Err
00136 if not err then
00137 err = 1
00138 end
00139
00140 TIO_write('Error (' .. err .. '):')
00141 TIO_writeln(Str)
00142 return err
00143 end
00144
00145 --! \brief run system command
00146 local function TOS_system(Cmd)
00147 local errno,str
00148 local rtn = os.execute(Cmd)
00149 if not (rtn==0) then
00150 errno = rtn
00151 str = 'an error occured'
00152 end
00153 return errno,str
00154 end
00155
00156 --! \brief does file exist?
00157 local function TOS_fileExists(Filename)
00158 local fh = io.open(Filename,'r')
00159 if fh~=nil then
00160 fh:close()
00161 return true
00162 end
00163 return false
00164 end
00165
00166 --! \brief get the current time
00167 local function TClock_GetTimeNow()
00168 if os.gettimeofday then
00169 return os.gettimeofday()
00170 else
00171 return os.time()
00172 end
00173 end
00174
00175 --! \brief get a timestamp
00176 --!
00177 --! not strictly necessary here but lets us put a timestamp on the end of the output stream.
00178 --! Note that doxygen won't read this, and being off the end of the true file length (num lines),
00179 --! it will have no effect.
00180 --! However it lets us check the output file tail when debugging.
00181 --!
00182 local function TClock_getTimeStamp()
00183 local now = TClock_GetTimeNow()
00184 local fraction_secs = now - math.floor(now)
00185 return os.date('%c %Z',now) .. ':' .. fraction_secs
00186 end
00187
00188 --! \brief trims a string from both ends
00189 local function TString_trim(Str)
00190 return Str:match("^%s*(.-)%s*$")
00191 end
00192
00193 --! \brief split a string
00194 --!
00195 --! \param Str
00196 --! \param Pattern
00197 --! \returns table of string fragments
00198 local function TString_split(Str, Pattern)
00199 local splitStr = {}
00200 local fpat = "(.-)" .. Pattern
00201 local last_end = 1
00202 local str, e, cap = string.find(Str,fpat, 1)
00203 while str do
00204 if str ~= 1 or cap ~= "" then
00205 table.insert(splitStr,cap)
00206 end
00207 last_end = e+1
00208 str, e, cap = string.find(Str,fpat, last_end)
00209 end
00210 if last_end <= #Str then
00211 cap = string.sub(Str,last_end)
00212 table.insert(splitStr, cap)
00213 end
00214 return splitStr
00215 end
00216
00217 --! \brief trim comment off end of string
00218 --!
00219 --! If the string has a comment on the end, this trims it off.
00220 --!
00221 local function TString_removeCommentFromLine(Line)
00222 local pos_comment = string.find(Line,'%-%-')
00223 if pos_comment then
00224 Line = string.sub(Line,1,pos_comment-1)
00225 end
00226 return Line
00227 end
00228
00229 local TClassList_methods = {}
00230 --! \brief get methods
00231 local function TClassList_method_get(Klass)
00232 return TClassList_methods[Klass]
00233 end
00234
00235 --! \brief add a method to list of known methods
00236 local function TClassList_method_add(Klass,Method)
00237 local classRec = TClassList_methods[Klass]
00238 if not classRec then
00239 TClassList_methods[Klass] = {}
00240 classRec = TClassList_methods[Klass]
00241 end
00242 table.insert(classRec,Method)
00243 end
00244
00245
00246 local TCommentary_fh
00247
00248 --! \brief write to output file
00249 local function TCommentary_writeln(Str)
00250 if TCommentary_fh then
00251 TCommentary_fh:write(Str .. '\n')
00252 end
00253 end
00254
00255 local TCommentary_fileID
00256 --! \brief open the commentary save file
00257 local function TCommentary_open(Filename,InputLuaFilename)
00258 TCommentary_fileID = '"' .. InputLuaFilename .. '" : ' .. TClock_getTimeStamp()
00259 TCommentary_fh = io.open(Filename,'a+')
00260 if TCommentary_fh then
00261 TCommentary_writeln('
00262 end
00263 end
00264
00265 --! \brief close the methods save file
00266 local function TCommentary_close()
00267 if TCommentary_fh then
00268 TCommentary_writeln('
00269 TCommentary_fh:close()
00270 TCommentary_fh = nil
00271 end
00272 end
00273
00274 --! \brief read stuff from save file
00275 local function TCommentary_readFileContents(Filename)
00276 if TOS_fileExists(Filename) then
00277 local klass,method,dot
00278 local cmd,colon
00279 local k,v,equals
00280 for line in io.lines(Filename) do
00281 if string.sub(line,1,2)=='
00282 -- it's a comment
00283 else
00284 colon = string.find(line,':')
00285 if colon then
00286 cmd = string.sub(line,1,colon)
00287 line = string.sub(line,colon+1)
00288 else
00289 cmd = nil
00290 end
00291 if (cmd == 'method:') then
00292 dot = string.find(line,'%.')
00293 klass = string.sub(line,1,dot-1)
00294 method = string.sub(line,dot+1)
00295 TClassList_method_add(klass,method)
00296 elseif(cmd == 'set:') then
00297 equals = string.find(line,'=')
00298 if equals then
00299 k = string.sub(line,1,equals-1)
00300 v = string.sub(line,equals+1)
00301 TConfig_set(k,v)
00302 else
00303 TConfig_set(line,true)
00304 end
00305 else -- ignore
00306 TIO_write('')
00307 end
00308 end
00309 end
00310 else
00311 TIO_write('')
00312 end
00313 end
00314
00315 --! \brief method to save file
00316 local function TCommentary_addMethod(Klass,Method)
00317 if TCommentary_fh then
00318 if Klass and Method then
00319 if string.find(Klass,'%.') or string.find(Method,'%.') then
00320 -- iffy, so we discard it
00321 else
00322 TCommentary_writeln('method:' .. Klass .. '.' .. Method)
00323 end
00324 end
00325 end
00326 end
00327
00328 --! \brief output line to stream
00329 --!
00330 --! Wraps IO_writeln()
00331 local function TIO_out2stream(Line)
00332 TIO_writeln(Line)
00333 end
00334
00335 --! \brief suppress line
00336 local function TIO_out2stream_commented(Line,Prefix,Suffix)
00337 local line = Line
00338 if (not Prefix) then
00339 Prefix=''
00340 end
00341 line = Prefix .. ':' .. line
00342
00343 if (Suffix) then
00344 line = line .. Suffix
00345 end
00346 TIO_out2stream('
00347 end
00348
00349
00350 TCommandline_argv = {}
00351 --! \brief setup/parse the commandline
00352 local function TCommandline_setup()
00353 local argv1 =arg[1]
00354 if not argv1 then
00355 argv1 = 'base'
00356 end
00357 TCommandline_argv_appname = argv1
00358
00359 local i=2
00360 local argvi=1
00361 while (argvi) do
00362 argvi = arg[i]
00363 if argvi then
00364 TCommandline_argv[i-1] = argvi
00365 i = i + 1
00366 end
00367 end
00368 end
00369 --! setup the commandline now
00370 TCommandline_setup()
00371
00372 --! \brief get commandline args
00373 local function TCommandline_getargv()
00374 return TCommandline_argv
00375 end
00376
00377 --! \brief get appname
00378 local function TApp_get_appname()
00379 return TCommandline_argv_appname
00380 end
00381
00382 --! \brief hack converter from lua to a pseudoC-ish language for doxygen
00383 --!
00384 --! This is a hack to make lua readable to doxygen.
00385 --!
00386 --! It works well enough to document functions/methods and classes, but not assignments.
00387 --! Our pseudo-C gets confused if we allow assginments to be shown.
00388 --! Because these are less interesting than class/functions/methods I have decided to
00389 --! live with this limitation.
00390 --!
00391 local function TApp_lua2dox(FileContents)
00392 local err
00393 local lines = FileContents
00394 local maxi=#lines
00395
00396 local i = 1
00397 local line,head
00398 while not err and (i<=maxi) do
00399 line = TString_trim(lines[i])
00400 if #line==0 then
00401 TIO_out2stream()
00402 elseif string.sub(line,1,2)=='--' then
00403 -- it's a comment of some kind
00404 if string.sub(line,1,3)=='--!' then
00405 -- it's a magic comment
00406 TIO_out2stream('
00407 elseif string.sub(line,1,4)=='--[[' then
00408 -- it's a multiline comment
00409 -- read lines to end of comment
00410 local hitend
00411 local comment = ''
00412 line = string.sub(line,5) --.. sep
00413 while not err and (i<maxi) and not hitend do
00414 comment = comment .. line .. '\n'
00415 line = lines[i+1]
00416 --if (string.sub(TString_trim(line),1,2)==']]') then
00417 if (string.find(line,'\]\]')) then
00418 local pos_close = string.find(line,'\]\]')
00419 comment = comment .. string.sub(line,1,pos_close-1)
00420 hitend=true
00421 end
00422 i = i + 1
00423 end
00424 -- got long comment
00425 if string.sub(comment,1,1)=='!' then
00426 TIO_out2stream('')
00427 else
00428 TIO_out2stream('')
00429 end
00430 else
00431 -- it's a boring comment
00432 TIO_out2stream_commented(line,'--')
00433 end
00434 elseif string.find(line,'^function%s') or string.find(line,'^local%s+function%s')then
00435 -- "function wibble..."
00436 -- it's a function declaration
00437 -- ....v...
00438 local pos_fn = string.find(line,'function')
00439 if (pos_fn) then
00440 local pos_local = string.find(line,'^local%s+function%s')
00441 local fn = TString_removeCommentFromLine(TString_trim(string.sub(line,pos_fn+8)))
00442
00443 if (string.sub(fn,1,1)=='(') then
00444 -- anonymous function
00445 TIO_out2stream_commented(line,'anon fn')
00446 else
00447 --[[
00448 we might have extracted a fn def with parameters on multilines
00449 The hack is to insert a new close paren with a note to that effect.
00450 ]]
00451
00452 if not string.find(fn,'%)') then
00453 fn = fn .. ' ___MissingCloseParenHere___)'
00454 end
00455
00456 local plain_fn_str = 'function ' .. fn .. '{}'
00457 if pos_local then
00458 plain_fn_str = 'local ' .. plain_fn_str
00459 end
00460
00461 local dot = string.find(fn,'%.')
00462 if dot then -- it's a method
00463 local klass = string.sub(fn,1,dot-1)
00464 local method = string.sub(fn,dot+1)
00465 local method_str = klass .. '::' .. method .. '{}'
00466 TCommentary_addMethod(klass,method)
00467 TIO_out2stream(method_str)
00468 else
00469 TIO_out2stream(plain_fn_str)
00470 end
00471 end
00472 end
00473 elseif string.find(line,'=%s+class%(') then
00474 -- it's a class definition
00475 line = TString_removeCommentFromLine(line)
00476 local klass,parent,pos_class
00477 -- ....v...
00478 pos_class = string.find(line,'=%s+class%(')
00479 klass = TString_trim(string.sub(line,1,pos_class-1))
00480 parent = TString_trim(string.sub(line,pos_class+8))
00481 parent = string.sub(parent,1,-2)
00482
00483 line = 'class ' .. klass
00484 if (#parent>0) then
00485 line = line .. ' :public ' .. parent
00486 end
00487
00488 -- need methods list
00489 local methods = TClassList_method_get(klass)
00490 local methods_str
00491 if methods then
00492 methods_str = 'public: '
00493 for k,v in pairs(methods) do
00494 methods_str = methods_str .. v .. ';'
00495 end
00496 else
00497 methods_str = ''
00498 end
00499 line = line .. '{' .. methods_str .. '}'
00500
00501 TIO_out2stream(line .. ';',true)
00502 else
00503 -- we don't know what this line means, so we can probably just comment it out
00504 TIO_out2stream_commented(line)
00505 end
00506
00507 i = i + 1
00508 end
00509 return err
00510 end
00511
00512 --! \brief run the filter
00513 --!
00514 --! \param AppTimestamp application + timestamp for this run
00515 --! \param Filename the filename or if nil stdin
00516 --! \param CommentaryFiles names of commentary files
00517 --! \return err or nil
00518 --!
00519 local function TApp_run_filter(AppTimestamp,Filename,CommentaryFiles)
00520 local err
00521 local filecontents
00522 if Filename then
00523 -- syphon lines to our table
00524 filecontents={}
00525 for line in io.lines(Filename) do
00526 table.insert(filecontents,line)
00527 end
00528 else
00529 -- get stuff from stdin as a long string (with crlfs etc)
00530 filecontents=io.read('*a')
00531 -- make it a table of lines
00532 filecontents = TString_split(filecontents,'[\n]') -- note this only works for unix files.
00533 Filename = 'stdin'
00534 end
00535
00536 if filecontents then
00537 TCommentary_readFileContents(CommentaryFiles.infile)
00538 TCommentary_open(CommentaryFiles.outfile,Filename)
00539
00540 err = TApp_lua2dox(filecontents)
00541
00542 TCommentary_close()
00543
00544 TIO_writeln('
00545 else
00546 err = TIO_showError(1,'couldn\'t find any file contents')
00547 end
00548 return err
00549 end
00550
00551 --! \brief run doxygen for one
00552 --!
00553 --! \param AppTimestamp application + timestamp for this run
00554 --! \param Argv commandline for this run
00555 --! \param CommentaryFiles names of commentary files
00556 --! \return err or nil
00557 --!
00558 local function TApp_run_doxygen(AppTimestamp,Argv,CommentaryFiles)
00559 local err
00560 TIO_writeln('running: ' .. AppTimestamp)
00561
00562 local argv1 = Argv[1]
00563 if argv1=='--help' then
00564 local appname = TApp_get_appname()
00565 TIO_writeln('Syntax:')
00566 TIO_writeln(' ' .. appname .. ' [[-g] [-s]] [<Doxyfile name>]|--help')
00567 TIO_writeln(' --help show this text')
00568 TIO_writeln(' -g: generate new Doxyfile')
00569 TIO_writeln(' -s: generate new Doxyfile without comments')
00570 TIO_writeln(' <Doxyfile name>: name of Doxyfile')
00571
00572 TIO_writeln()
00573 TIO_writeln(' For help on doxygen run its help system directly')
00574 --! \todo more help here
00575 else
00576 local cl = 'doxygen'
00577 for i,argv_i in ipairs(Argv) do
00578 if i>=1 then -- don't want to use this app's name
00579 cl = cl .. ' ' .. argv_i
00580 end
00581 end
00582
00583 TIO_writeln('about to run "' .. cl .. '"')
00584 err = TOS_system(cl)
00585
00586 if not err then
00587 -- cycle commentary files
00588 local newComments=CommentaryFiles.outfile
00589 local nextRunsComments=CommentaryFiles.infile
00590 if TOS_fileExists(newComments) then
00591 TIO_writeln('found outfile "' .. newComments .. '"')
00592 os.remove(nextRunsComments)
00593 TIO_writeln('mv "' .. newComments .. '"-> ' .. nextRunsComments .. '"')
00594 os.rename(newComments,nextRunsComments)
00595 end
00596 end
00597 end
00598 return err
00599 end
00600
00601 -- main
00602 local timestamp = TClock_getTimeStamp()
00603 local appname = TApp_get_appname()
00604 local version = '0.1 20120704'
00605 local appTimestamp = appname .. '(v' .. version .. ') :' .. timestamp
00606 local commentary_files = {
00607 infile=TConfig_get('LUA2DOX_COMMENTARY_FILE_IN')
00608 ,outfile=TConfig_get('LUA2DOX_COMMENTARY_FILE_OUT')
00609 }
00610
00611 if appname == 'lua2dox_filter' then
00612 err = TApp_run_filter(appTimestamp,TCommandline_getargv()[1],commentary_files)
00613 TIO_writeln('
00614 else
00615 err = TApp_run_doxygen(appTimestamp,TCommandline_getargv(),commentary_files)
00616 end
00617
00618 --eof