lua2dox.lua 17 KB

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