lua2dox.lua 17 KB

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