lua2dox.lua 19 KB

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