lua2dox  0.2 20130128
lua2dox.lua
Go to the documentation of this file.
1 --[[--------------------------------------------------------------------------
2  -- Copyright (C) 2012 by Simon Dales --
3  -- simon@purrsoft.co.uk --
4  -- --
5  -- This program is free software; you can redistribute it and/or modify --
6  -- it under the terms of the GNU General Public License as published by --
7  -- the Free Software Foundation; either version 2 of the License, or --
8  -- (at your option) any later version. --
9  -- --
10  -- This program is distributed in the hope that it will be useful, --
11  -- but WITHOUT ANY WARRANTY; without even the implied warranty of --
12  -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --
13  -- GNU General Public License for more details. --
14  -- --
15  -- You should have received a copy of the GNU General Public License --
16  -- along with this program; if not, write to the --
17  -- Free Software Foundation, Inc., --
18  -- 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. --
19 ----------------------------------------------------------------------------]]
20 --[[!
21  \file
22  \brief a hack lua2dox converter
23  ]]
24 
25 --[[!
26  \mainpage
27 
28  Introduction
29  ------------
30 
31  A hack lua2dox converter
32  Version 0.2
33 
34  This lets us make Doxygen output some documentation to let
35  us develop this code.
36 
37  It is partially cribbed from the functionality of lua2dox
38  (http://search.cpan.org/~alec/Doxygen-Lua-0.02/lib/Doxygen/Lua.pm).
39  Found on CPAN when looking for something else; kinda handy.
40 
41  Improved from lua2dox to make the doxygen output more friendly.
42  Also it runs faster in lua rather than Perl.
43 
44  Because this Perl based system is called "lua2dox"., I have decided to add ".lua" to the name
45  to keep the two separate.
46 
47  Running
48  -------
49 
50  <ol>
51  <li> Ensure doxygen is installed on your system and that you are familiar with its use.
52  Best is to try to make and document some simple C/C++/PHP to see what it produces.
53  You can experiment with the enclosed example code.
54 
55  <li> Run "doxygen -g" to create a default Doxyfile.
56 
57  Then alter it to let it recognise lua. Add the two following lines:
58 
59  \code{.bash}
60  FILE_PATTERNS = *.lua
61 
62  FILTER_PATTERNS = *.lua=lua2dox_filter
63  \endcode
64 
65 
66  Either add them to the end or find the appropriate entry in Doxyfile.
67 
68  There are other lines that you might like to alter, but see futher documentation for details.
69 
70  <li> When Doxyfile is edited run "doxygen"
71 
72  The core function reads the input file (filename or stdin) and outputs some pseudo C-ish language.
73  It only has to be good enough for doxygen to see it as legal.
74  Therefore our lua interpreter is fairly limited, but "good enough".
75 
76  One limitation is that each line is treated separately (except for long comments).
77  The implication is that class and function declarations must be on the same line.
78  Some functions can have their parameter lists extended over multiple lines to make it look neat.
79  Managing this where there are also some comments is a bit more coding than I want to do at this stage,
80  so it will probably not document accurately if we do do this.
81 
82  However I have put in a hack that will insert the "missing" close paren.
83  The effect is that you will get the function documented, but not with the parameter list you might expect.
84  </ol>
85 
86  Installation
87  ------------
88 
89  Here for linux or unix-like, for any other OS you need to refer to other documentation.
90 
91  This file is "lua2dox.lua". It gets called by "lua2dox_filter"(bash).
92  Somewhere in your path (e.g. "~/bin" or "/usr/local/bin") put a link to "lua2dox_filter".
93 
94  Documentation
95  -------------
96 
97  Read the external documentation that should be part of this package.
98  For example look for the "README" and some .PDFs.
99 
100  ]]
101 
102 -- we won't use our library code, so this becomes more portable
103 
104 -- require 'elijah_fix_require'
105 -- require 'elijah_class'
106 --
107 --! \brief ``declare'' as class
108 --!
109 --! use as:
110 --! \code{.lua}
111 --! TWibble = class()
112 --! function TWibble.init(this,Str)
113 --! this.str = Str
114 --! -- more stuff here
115 --! end
116 --! \endcode
117 --!
118 function class(BaseClass, ClassInitialiser)
119  local newClass = {} -- a new class newClass
120  if not ClassInitialiser and type(BaseClass) == 'function' then
121  ClassInitialiser = BaseClass
122  BaseClass = nil
123  elseif type(BaseClass) == 'table' then
124  -- our new class is a shallow copy of the base class!
125  for i,v in pairs(BaseClass) do
126  newClass[i] = v
127  end
128  newClass._base = BaseClass
129  end
130  -- the class will be the metatable for all its newInstanceects,
131  -- and they will look up their methods in it.
132  newClass.__index = newClass
133 
134  -- expose a constructor which can be called by <classname>(<args>)
135  local classMetatable = {}
136  classMetatable.__call =
137  function(class_tbl, ...)
138  local newInstance = {}
139  setmetatable(newInstance,newClass)
140  --if init then
141  -- init(newInstance,...)
142  if class_tbl.init then
143  class_tbl.init(newInstance,...)
144  else
145  -- make sure that any stuff from the base class is initialized!
146  if BaseClass and BaseClass.init then
147  BaseClass.init(newInstance, ...)
148  end
149  end
150  return newInstance
151  end
152  newClass.init = ClassInitialiser
153  newClass.is_a =
154  function(this, klass)
155  local thisMetatable = getmetatable(this)
156  while thisMetatable do
157  if thisMetatable == klass then
158  return true
159  end
160  thisMetatable = thisMetatable._base
161  end
162  return false
163  end
164  setmetatable(newClass, classMetatable)
165  return newClass
166 end
167 
168 -- require 'elijah_clock'
169 
170 --! \class TCore_Clock
171 --! \brief a clock
173 
174 --! \brief get the current time
176  if os.gettimeofday then
177  return os.gettimeofday()
178  else
179  return os.time()
180  end
181 end
182 
183 --! \brief constructor
184 function TCore_Clock.init(this,T0)
185  if T0 then
186  this.t0 = T0
187  else
188  this.t0 = TCore_Clock.GetTimeNow()
189  end
190 end
191 
192 --! \brief get time string
193 function TCore_Clock.getTimeStamp(this,T0)
194  local t0
195  if T0 then
196  t0 = T0
197  else
198  t0 = this.t0
199  end
200  return os.date('%c %Z',t0)
201 end
202 
203 
204 --require 'elijah_io'
205 
206 --! \class TCore_IO
207 --! \brief io to console
208 --!
209 --! pseudo class (no methods, just to keep documentation tidy)
211 --
212 --! \brief write to stdout
213 function TCore_IO_write(Str)
214  if (Str) then
215  io.write(Str)
216  end
217 end
218 
219 --! \brief write to stdout
220 function TCore_IO_writeln(Str)
221  if (Str) then
222  io.write(Str)
223  end
224  io.write("\n")
225 end
226 
227 
228 --require 'elijah_string'
229 
230 --! \brief trims a string
231 function string_trim(Str)
232  return Str:match("^%s*(.-)%s*$")
233 end
234 
235 --! \brief split a string
236 --!
237 --! \param Str
238 --! \param Pattern
239 --! \returns table of string fragments
240 function string_split(Str, Pattern)
241  local splitStr = {}
242  local fpat = "(.-)" .. Pattern
243  local last_end = 1
244  local str, e, cap = string.find(Str,fpat, 1)
245  while str do
246  if str ~= 1 or cap ~= "" then
247  table.insert(splitStr,cap)
248  end
249  last_end = e+1
250  str, e, cap = string.find(Str,fpat, last_end)
251  end
252  if last_end <= #Str then
253  cap = string.sub(Str,last_end)
254  table.insert(splitStr, cap)
255  end
256  return splitStr
257 end
258 
259 
260 --require 'elijah_commandline'
261 
262 --! \class TCore_Commandline
263 --! \brief reads/parses commandline
265 
266 --! \brief constructor
267 function TCore_Commandline.init(this)
268  this.argv = arg
269  this.parsed = {}
270  this.params = {}
271 end
272 
273 --! \brief get value
274 function TCore_Commandline.getRaw(this,Key,Default)
275  local val = this.argv[Key]
276  if not val then
277  val = Default
278  end
279  return val
280 end
281 
282 
283 --require 'elijah_debug'
284 
285 -------------------------------
286 --! \brief file buffer
287 --!
288 --! an input file buffer
289 TStream_Read = class()
290 
291 --! \brief get contents of file
292 --!
293 --! \param Filename name of file to read (or nil == stdin)
294 function TStream_Read.getContents(this,Filename)
295  -- get lines from file
296  local filecontents
297  if Filename then
298  -- syphon lines to our table
299  --TCore_Debug_show_var('Filename',Filename)
300  filecontents={}
301  for line in io.lines(Filename) do
302  table.insert(filecontents,line)
303  end
304  else
305  -- get stuff from stdin as a long string (with crlfs etc)
306  filecontents=io.read('*a')
307  -- make it a table of lines
308  filecontents = TString_split(filecontents,'[\n]') -- note this only works for unix files.
309  Filename = 'stdin'
310  end
311 
312  if filecontents then
313  this.filecontents = filecontents
314  this.contentsLen = #filecontents
315  this.currentLineNo = 1
316  end
317 
318  return filecontents
319 end
320 
321 --! \brief get lineno
322 function TStream_Read.getLineNo(this)
323  return this.currentLineNo
324 end
325 
326 --! \brief get a line
327 function TStream_Read.getLine(this)
328  local line
329  if this.currentLine then
330  line = this.currentLine
331  this.currentLine = nil
332  else
333  -- get line
334  if this.currentLineNo<=this.contentsLen then
335  line = this.filecontents[this.currentLineNo]
336  this.currentLineNo = this.currentLineNo + 1
337  else
338  line = ''
339  end
340  end
341  return line
342 end
343 
344 --! \brief save line fragment
345 function TStream_Read.ungetLine(this,LineFrag)
346  this.currentLine = LineFrag
347 end
348 
349 --! \brief is it eof?
350 function TStream_Read.eof(this)
351  if this.currentLine or this.currentLineNo<=this.contentsLen then
352  return false
353  end
354  return true
355 end
356 
357 --! \brief output stream
358 TStream_Write = class()
359 
360 --! \brief constructor
361 function TStream_Write.init(this)
362  this.tailLine = {}
363 end
364 
365 --! \brief write immediately
366 function TStream_Write.write(this,Str)
367  TCore_IO_write(Str)
368 end
369 
370 --! \brief write immediately
371 function TStream_Write.writeln(this,Str)
372  TCore_IO_writeln(Str)
373 end
374 
375 --! \brief write immediately
376 function TStream_Write.writelnComment(this,Str)
377  TCore_IO_write('// ZZ: ')
378  TCore_IO_writeln(Str)
379 end
380 
381 --! \brief write to tail
382 function TStream_Write.writelnTail(this,Line)
383  if not Line then
384  Line = ''
385  end
386  table.insert(this.tailLine,Line)
387 end
388 
389 --! \brief outout tail lines
391  for k,line in ipairs(this.tailLine) do
392  TCore_IO_writeln(line)
393  end
394  TCore_IO_write('// Lua2DoX new eof')
395 end
396 
397 --! \brief input filter
399 
400 --! \brief allow us to do errormessages
401 function TLua2DoX_filter.warning(this,Line,LineNo,Legend)
402  this.outStream:writelnTail(
403  '
404  )
405 end
406 
407 --! \brief trim comment off end of string
408 --!
409 --! If the string has a comment on the end, this trims it off.
410 --!
411 local function TString_removeCommentFromLine(Line)
412  local pos_comment = string.find(Line,'%-%-')
413  local tailComment
414  if pos_comment then
415  Line = string.sub(Line,1,pos_comment-1)
416  tailComment = string.sub(Line,pos_comment)
417  end
418  return Line,tailComment
419 end
420 
421 --! \brief get directive from magic
422 local function getMagicDirective(Line)
423  local macro,tail
424  local macroStr = '[\\\@]'
425  local pos_macro = string.find(Line,macroStr)
426  if pos_macro then
427  --! ....\\ macro...stuff
428  --! ....\@ macro...stuff
429  local line = string.sub(Line,pos_macro+1)
430  local space = string.find(line,'%s+')
431  if space then
432  macro = string.sub(line,1,space-1)
433  tail = string_trim(string.sub(line,space+1))
434  else
435  macro = line
436  tail = ''
437  end
438  end
439  return macro,tail
440 end
441 
442 --! \brief check comment for fn
443 local function checkComment4fn(Fn_magic,MagicLines)
444  local fn_magic = Fn_magic
445 -- TCore_IO_writeln('// checkComment4fn "' .. MagicLines .. '"')
446 
447  local magicLines = string_split(MagicLines,'\n')
448 
449  local macro,tail
450 
451  for k,line in ipairs(magicLines) do
452  macro,tail = getMagicDirective(line)
453  if macro == 'fn' then
454  fn_magic = tail
455  -- TCore_IO_writeln('// found fn "' .. fn_magic .. '"')
456  else
457  --TCore_IO_writeln('// not found fn "' .. line .. '"')
458  end
459  end
460 
461  return fn_magic
462 end
463 --! \brief run the filter
464 function TLua2DoX_filter.readfile(this,AppStamp,Filename)
465  local err
466 
467  local inStream = TStream_Read()
468  local outStream = TStream_Write()
469  this.outStream = outStream -- save to this obj
470 
471  if (inStream:getContents(Filename)) then
472  -- output the file
473  local line
474  local fn_magic -- function name/def from magic comment
475 
476  outStream:writelnTail('// #######################')
477  outStream:writelnTail('// app run:' .. AppStamp)
478  outStream:writelnTail('// #######################')
479  outStream:writelnTail()
480 
481  while not (err or inStream:eof()) do
482  line = string_trim(inStream:getLine())
483 -- TCore_Debug_show_var('inStream',inStream)
484 -- TCore_Debug_show_var('line',line )
485  if string.sub(line,1,2)=='--' then -- its a comment
486  if string.sub(line,3,3)=='!' then -- it's a magic comment
487  local magic = string.sub(line,4)
488  outStream:writeln('
489  fn_magic = checkComment4fn(fn_magic,magic)
490  elseif string.sub(line,3,4)=='[[' then -- it's a long comment
491  line = string.sub(line,5) -- nibble head
492  local comment = ''
493  local closeSquare,hitend,thisComment
494  while (not err) and (not hitend) and (not inStream:eof()) do
495  closeSquare = string.find(line,']]')
496  if not closeSquare then -- need to look on another line
497  thisComment = line .. '\n'
498  line = inStream:getLine()
499  else
500  thisComment = string.sub(line,1,closeSquare-1)
501  hitend = true
502 
503  -- unget the tail of the line
504  -- in most cases it's empty. This may make us less efficient but
505  -- easier to program
506  inStream:ungetLine(string_trim(string.sub(line,closeSquare+2)))
507  end
508  comment = comment .. thisComment
509  end
510  if string.sub(comment,1,1)=='!' then -- it's a long magic comment
511  outStream:write('/*' .. comment .. '*/ ')
512  fn_magic = checkComment4fn(fn_magic,comment)
513  else -- discard
514  outStream:write('/* zz:' .. comment .. '*/ ')
515  fn_magic = nil
516  end
517  else
518  outStream:writeln('// zz:"' .. line .. '"')
519  fn_magic = nil
520  end
521  elseif string.find(line,'^function') or string.find(line,'^local%s+function') then
522  -- it's a function
523  local pos_fn = string.find(line,'function')
524  -- function
525  -- ....v...
526  if pos_fn then
527  -- we've got a function
528  local fn_type
529  if string.find(line,'^local%s+') then
530  fn_type = 'static '
531  else
532  fn_type = ''
533  end
534  local fn = TString_removeCommentFromLine(string_trim(string.sub(line,pos_fn+8)))
535  if fn_magic then
536  fn = fn_magic
537  fn_magic = nil
538  end
539 
540  if string.sub(fn,1,1)=='(' then
541  -- it's an anonymous function
542  outStream:writelnComment(line)
543  else
544  -- fn has a name, so is interesting
545 
546  -- want to fix for iffy declarations
547  local open_paren = string.find(fn,'[%({]')
548  local fn0 = fn
549  if open_paren then
550  fn0 = string.sub(fn,1,open_paren-1)
551  -- we might have a missing close paren
552  if not string.find(fn,'%)') then
553  fn = fn .. ' ___MissingCloseParenHere___)'
554  end
555  end
556 
557  local dot = string.find(fn0,'[%.:]')
558  if dot then -- it's a method
559  local klass = string.sub(fn,1,dot-1)
560  local method = string.sub(fn,dot+1)
561  --TCore_IO_writeln('function ' .. klass .. '::' .. method .. ftail .. '{}')
562  --TCore_IO_writeln(klass .. '::' .. method .. ftail .. '{}')
563  outStream:writeln(
564  '/*! \\memberof ' .. klass .. ' */ '
565  .. method .. '{}'
566  )
567  else
568  -- add vanilla function
569 
570  outStream:writeln(fn_type .. 'function ' .. fn .. '{}')
571  end
572  end
573  else
574  this:warning(inStream:getLineNo(),'something weird here')
575  end
576  fn_magic = nil -- mustn't indavertently use it again
577  elseif string.find(line,'=%s*class%(') then
578  -- it's a class declaration
579  local tailComment
580  line,tailComment = TString_removeCommentFromLine(line)
581  local equals = string.find(line,'=')
582  local klass = string_trim(string.sub(line,1,equals-1))
583  local tail = string_trim(string.sub(line,equals+1))
584  -- class(wibble wibble)
585  -- ....v.
586  local parent = string.sub(tail,7,-2)
587  if #parent>0 then
588  parent = ' :public ' .. parent
589  end
590  outStream:writeln('class ' .. klass .. parent .. '{};')
591  else
592  -- we don't know what this line means, so we can probably just comment it out
593  if #line>0 then
594  outStream:writeln('// zz: ' .. line)
595  else
596  outStream:writeln() -- keep this line blank
597  end
598  end
599  end
600 
601  -- output the tail
602  outStream:write_tailLines()
603  else
604  outStream:writeln('!empty file')
605  end
606 end
607 
608 --! \brief this application
609 TApp = class()
610 
611 --! \brief constructor
612 function TApp.init(this)
613  local t0 = TCore_Clock()
614  this.timestamp = t0:getTimeStamp()
615  this.name = 'Lua2DoX'
616  this.version = '0.2 20130128'
617  this.copyright = 'Copyright (c) Simon Dales 2012-13'
618 end
619 
620 function TApp.getRunStamp(this)
621  return this.name .. ' (' .. this.version .. ') '
622  .. this.timestamp
623 end
624 
625 function TApp.getVersion(this)
626  return this.name .. ' (' .. this.version .. ') '
627 end
628 
629 function TApp.getCopyright(this)
630  return this.copyright
631 end
632 
633 local This_app = TApp()
634 
635 --main
636 local cl = TCore_Commandline()
637 
638 local argv1 = cl:getRaw(2)
639 if argv1 == '--help' then
640  TCore_IO_writeln(This_app:getVersion())
641  TCore_IO_writeln(This_app:getCopyright())
643 run as:
644 lua2dox_filter <param>
645 --------------
646 Param:
647  <filename> : interprets filename
648  --version : show version/copyright info
649  --help : this help text]])
650 elseif argv1 == '--version' then
651  TCore_IO_writeln(This_app:getVersion())
652  TCore_IO_writeln(This_app:getCopyright())
653 else
654  -- it's a filter
655  local appStamp = This_app:getRunStamp()
656  local filename = argv1
657 
658  local filter = TLua2DoX_filter()
659  filter:readfile(appStamp,filename)
660 end
661 
662 
663 --eof