gen_vimdoc.py 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279
  1. #!/usr/bin/env python3
  2. """Generates Nvim :help docs from C/Lua docstrings, using Doxygen.
  3. Also generates *.mpack files. To inspect the *.mpack structure:
  4. :new | put=v:lua.vim.inspect(v:lua.vim.mpack.unpack(readfile('runtime/doc/api.mpack','B')))
  5. Flow:
  6. main
  7. extract_from_xml
  8. fmt_node_as_vimhelp \
  9. para_as_map } recursive
  10. update_params_map /
  11. render_node
  12. TODO: eliminate this script and use Lua+treesitter (requires parsers for C and
  13. Lua markdown-style docstrings).
  14. The generated :help text for each function is formatted as follows:
  15. - Max width of 78 columns (`text_width`).
  16. - Indent with spaces (not tabs).
  17. - Indent of 4 columns for body text (`indentation`).
  18. - Function signature and helptag (right-aligned) on the same line.
  19. - Signature and helptag must have a minimum of 8 spaces between them.
  20. - If the signature is too long, it is placed on the line after the helptag.
  21. Signature wraps at `text_width - 8` characters with subsequent
  22. lines indented to the open parenthesis.
  23. - Subsection bodies are indented an additional 4 spaces.
  24. - Body consists of function description, parameters, return description, and
  25. C declaration (`INCLUDE_C_DECL`).
  26. - Parameters are omitted for the `void` and `Error *` types, or if the
  27. parameter is marked as [out].
  28. - Each function documentation is separated by a single line.
  29. """
  30. import argparse
  31. import os
  32. import re
  33. import sys
  34. import shutil
  35. import textwrap
  36. import subprocess
  37. import collections
  38. import msgpack
  39. import logging
  40. from pathlib import Path
  41. from xml.dom import minidom
  42. MIN_PYTHON_VERSION = (3, 6)
  43. MIN_DOXYGEN_VERSION = (1, 9, 0)
  44. if sys.version_info < MIN_PYTHON_VERSION:
  45. print("requires Python {}.{}+".format(*MIN_PYTHON_VERSION))
  46. sys.exit(1)
  47. doxygen_version = tuple((int(i) for i in subprocess.check_output(["doxygen", "-v"],
  48. universal_newlines=True).split()[0].split('.')))
  49. if doxygen_version < MIN_DOXYGEN_VERSION:
  50. print("\nRequires doxygen {}.{}.{}+".format(*MIN_DOXYGEN_VERSION))
  51. print("Your doxygen version is {}.{}.{}\n".format(*doxygen_version))
  52. sys.exit(1)
  53. # Need a `nvim` that supports `-l`, try the local build
  54. nvim_path = Path(__file__).parent / "../build/bin/nvim"
  55. if nvim_path.exists():
  56. nvim = str(nvim_path)
  57. else:
  58. # Until 0.9 is released, use this hacky way to check that "nvim -l foo.lua" works.
  59. nvim_out = subprocess.check_output(['nvim', '-h'], universal_newlines=True)
  60. nvim_version = [line for line in nvim_out.split('\n')
  61. if '-l ' in line]
  62. if len(nvim_version) == 0:
  63. print((
  64. "\nYou need to have a local Neovim build or a `nvim` version 0.9 for `-l` "
  65. "support to build the documentation."))
  66. sys.exit(1)
  67. nvim = 'nvim'
  68. # DEBUG = ('DEBUG' in os.environ)
  69. INCLUDE_C_DECL = ('INCLUDE_C_DECL' in os.environ)
  70. INCLUDE_DEPRECATED = ('INCLUDE_DEPRECATED' in os.environ)
  71. log = logging.getLogger(__name__)
  72. LOG_LEVELS = {
  73. logging.getLevelName(level): level for level in [
  74. logging.DEBUG, logging.INFO, logging.ERROR
  75. ]
  76. }
  77. text_width = 78
  78. indentation = 4
  79. script_path = os.path.abspath(__file__)
  80. base_dir = os.path.dirname(os.path.dirname(script_path))
  81. out_dir = os.path.join(base_dir, 'tmp-{target}-doc')
  82. filter_cmd = '%s %s' % (sys.executable, script_path)
  83. msgs = [] # Messages to show on exit.
  84. lua2dox = os.path.join(base_dir, 'scripts', 'lua2dox.lua')
  85. CONFIG = {
  86. 'api': {
  87. 'mode': 'c',
  88. 'filename': 'api.txt',
  89. # Section ordering.
  90. 'section_order': [
  91. 'vim.c',
  92. 'vimscript.c',
  93. 'command.c',
  94. 'options.c',
  95. 'buffer.c',
  96. 'extmark.c',
  97. 'window.c',
  98. 'win_config.c',
  99. 'tabpage.c',
  100. 'autocmd.c',
  101. 'ui.c',
  102. ],
  103. # List of files/directories for doxygen to read, relative to `base_dir`
  104. 'files': ['src/nvim/api'],
  105. # file patterns used by doxygen
  106. 'file_patterns': '*.h *.c',
  107. # Only function with this prefix are considered
  108. 'fn_name_prefix': 'nvim_',
  109. # Section name overrides.
  110. 'section_name': {
  111. 'vim.c': 'Global',
  112. },
  113. # For generated section names.
  114. 'section_fmt': lambda name: f'{name} Functions',
  115. # Section helptag.
  116. 'helptag_fmt': lambda name: f'*api-{name.lower()}*',
  117. # Per-function helptag.
  118. 'fn_helptag_fmt': lambda fstem, name: f'*{name}()*',
  119. # Module name overrides (for Lua).
  120. 'module_override': {},
  121. # Append the docs for these modules, do not start a new section.
  122. 'append_only': [],
  123. },
  124. 'lua': {
  125. 'mode': 'lua',
  126. 'filename': 'lua.txt',
  127. 'section_order': [
  128. '_editor.lua',
  129. '_inspector.lua',
  130. 'shared.lua',
  131. 'loader.lua',
  132. 'uri.lua',
  133. 'ui.lua',
  134. 'filetype.lua',
  135. 'keymap.lua',
  136. 'fs.lua',
  137. 'secure.lua',
  138. 'version.lua',
  139. ],
  140. 'files': [
  141. 'runtime/lua/vim/_editor.lua',
  142. 'runtime/lua/vim/shared.lua',
  143. 'runtime/lua/vim/loader.lua',
  144. 'runtime/lua/vim/uri.lua',
  145. 'runtime/lua/vim/ui.lua',
  146. 'runtime/lua/vim/filetype.lua',
  147. 'runtime/lua/vim/keymap.lua',
  148. 'runtime/lua/vim/fs.lua',
  149. 'runtime/lua/vim/secure.lua',
  150. 'runtime/lua/vim/version.lua',
  151. 'runtime/lua/vim/_inspector.lua',
  152. ],
  153. 'file_patterns': '*.lua',
  154. 'fn_name_prefix': '',
  155. 'section_name': {
  156. 'lsp.lua': 'core',
  157. '_inspector.lua': 'inspector',
  158. },
  159. 'section_fmt': lambda name: (
  160. 'Lua module: vim'
  161. if name.lower() == '_editor'
  162. else f'Lua module: {name.lower()}'),
  163. 'helptag_fmt': lambda name: (
  164. '*lua-vim*'
  165. if name.lower() == '_editor'
  166. else f'*lua-{name.lower()}*'),
  167. 'fn_helptag_fmt': lambda fstem, name: (
  168. f'*vim.{name}()*'
  169. if fstem.lower() == '_editor'
  170. else f'*{fstem}.{name}()*'),
  171. 'module_override': {
  172. # `shared` functions are exposed on the `vim` module.
  173. 'shared': 'vim',
  174. '_inspector': 'vim',
  175. 'uri': 'vim',
  176. 'ui': 'vim.ui',
  177. 'loader': 'vim.loader',
  178. 'filetype': 'vim.filetype',
  179. 'keymap': 'vim.keymap',
  180. 'fs': 'vim.fs',
  181. 'secure': 'vim.secure',
  182. 'version': 'vim.version',
  183. },
  184. 'append_only': [
  185. 'shared.lua',
  186. ],
  187. },
  188. 'lsp': {
  189. 'mode': 'lua',
  190. 'filename': 'lsp.txt',
  191. 'section_order': [
  192. 'lsp.lua',
  193. 'buf.lua',
  194. 'diagnostic.lua',
  195. 'codelens.lua',
  196. 'tagfunc.lua',
  197. 'semantic_tokens.lua',
  198. 'handlers.lua',
  199. 'util.lua',
  200. 'log.lua',
  201. 'rpc.lua',
  202. 'protocol.lua',
  203. ],
  204. 'files': [
  205. 'runtime/lua/vim/lsp',
  206. 'runtime/lua/vim/lsp.lua',
  207. ],
  208. 'file_patterns': '*.lua',
  209. 'fn_name_prefix': '',
  210. 'section_name': {'lsp.lua': 'lsp'},
  211. 'section_fmt': lambda name: (
  212. 'Lua module: vim.lsp'
  213. if name.lower() == 'lsp'
  214. else f'Lua module: vim.lsp.{name.lower()}'),
  215. 'helptag_fmt': lambda name: (
  216. '*lsp-core*'
  217. if name.lower() == 'lsp'
  218. else f'*lsp-{name.lower()}*'),
  219. 'fn_helptag_fmt': lambda fstem, name: (
  220. f'*vim.lsp.{name}()*'
  221. if fstem == 'lsp' and name != 'client'
  222. else (
  223. '*vim.lsp.client*'
  224. # HACK. TODO(justinmk): class/structure support in lua2dox
  225. if 'lsp.client' == f'{fstem}.{name}'
  226. else f'*vim.lsp.{fstem}.{name}()*')),
  227. 'module_override': {},
  228. 'append_only': [],
  229. },
  230. 'diagnostic': {
  231. 'mode': 'lua',
  232. 'filename': 'diagnostic.txt',
  233. 'section_order': [
  234. 'diagnostic.lua',
  235. ],
  236. 'files': ['runtime/lua/vim/diagnostic.lua'],
  237. 'file_patterns': '*.lua',
  238. 'fn_name_prefix': '',
  239. 'section_name': {'diagnostic.lua': 'diagnostic'},
  240. 'section_fmt': lambda _: 'Lua module: vim.diagnostic',
  241. 'helptag_fmt': lambda _: '*diagnostic-api*',
  242. 'fn_helptag_fmt': lambda fstem, name: f'*vim.{fstem}.{name}()*',
  243. 'module_override': {},
  244. 'append_only': [],
  245. },
  246. 'treesitter': {
  247. 'mode': 'lua',
  248. 'filename': 'treesitter.txt',
  249. 'section_order': [
  250. 'treesitter.lua',
  251. 'language.lua',
  252. 'query.lua',
  253. 'highlighter.lua',
  254. 'languagetree.lua',
  255. 'playground.lua',
  256. ],
  257. 'files': [
  258. 'runtime/lua/vim/treesitter.lua',
  259. 'runtime/lua/vim/treesitter/',
  260. ],
  261. 'file_patterns': '*.lua',
  262. 'fn_name_prefix': '',
  263. 'section_name': {},
  264. 'section_fmt': lambda name: (
  265. 'Lua module: vim.treesitter'
  266. if name.lower() == 'treesitter'
  267. else f'Lua module: vim.treesitter.{name.lower()}'),
  268. 'helptag_fmt': lambda name: (
  269. '*lua-treesitter-core*'
  270. if name.lower() == 'treesitter'
  271. else f'*lua-treesitter-{name.lower()}*'),
  272. 'fn_helptag_fmt': lambda fstem, name: (
  273. f'*vim.{fstem}.{name}()*'
  274. if fstem == 'treesitter'
  275. else f'*{name}()*'
  276. if name[0].isupper()
  277. else f'*vim.treesitter.{fstem}.{name}()*'),
  278. 'module_override': {},
  279. 'append_only': [],
  280. }
  281. }
  282. param_exclude = (
  283. 'channel_id',
  284. )
  285. # Annotations are displayed as line items after API function descriptions.
  286. annotation_map = {
  287. 'FUNC_API_FAST': '|api-fast|',
  288. 'FUNC_API_CHECK_TEXTLOCK': 'not allowed when |textlock| is active',
  289. 'FUNC_API_REMOTE_ONLY': '|RPC| only',
  290. 'FUNC_API_LUA_ONLY': 'Lua |vim.api| only',
  291. }
  292. # Raises an error with details about `o`, if `cond` is in object `o`,
  293. # or if `cond()` is callable and returns True.
  294. def debug_this(o, cond=True):
  295. name = ''
  296. if cond is False:
  297. return
  298. if not isinstance(o, str):
  299. try:
  300. name = o.nodeName
  301. o = o.toprettyxml(indent=' ', newl='\n')
  302. except Exception:
  303. pass
  304. if (cond is True
  305. or (callable(cond) and cond())
  306. or (not callable(cond) and cond in o)):
  307. raise RuntimeError('xxx: {}\n{}'.format(name, o))
  308. # Appends a message to a list which will be printed on exit.
  309. def msg(s):
  310. msgs.append(s)
  311. # Print all collected messages.
  312. def msg_report():
  313. for m in msgs:
  314. print(f' {m}')
  315. # Print collected messages, then throw an exception.
  316. def fail(s):
  317. msg_report()
  318. raise RuntimeError(s)
  319. def find_first(parent, name):
  320. """Finds the first matching node within parent."""
  321. sub = parent.getElementsByTagName(name)
  322. if not sub:
  323. return None
  324. return sub[0]
  325. def iter_children(parent, name):
  326. """Yields matching child nodes within parent."""
  327. for child in parent.childNodes:
  328. if child.nodeType == child.ELEMENT_NODE and child.nodeName == name:
  329. yield child
  330. def get_child(parent, name):
  331. """Gets the first matching child node."""
  332. for child in iter_children(parent, name):
  333. return child
  334. return None
  335. def self_or_child(n):
  336. """Gets the first child node, or self."""
  337. if len(n.childNodes) == 0:
  338. return n
  339. return n.childNodes[0]
  340. def align_tags(line):
  341. tag_regex = r"\s(\*.+?\*)(?:\s|$)"
  342. tags = re.findall(tag_regex, line)
  343. if len(tags) > 0:
  344. line = re.sub(tag_regex, "", line)
  345. tags = " " + " ".join(tags)
  346. line = line + (" " * (78 - len(line) - len(tags))) + tags
  347. return line
  348. def clean_lines(text):
  349. """Removes superfluous lines.
  350. The beginning and end of the string is trimmed. Empty lines are collapsed.
  351. """
  352. return re.sub(r'\A\n\s*\n*|\n\s*\n*\Z', '', re.sub(r'(\n\s*\n+)+', '\n\n', text))
  353. def is_blank(text):
  354. return '' == clean_lines(text)
  355. def get_text(n, preformatted=False):
  356. """Recursively concatenates all text in a node tree."""
  357. text = ''
  358. if n.nodeType == n.TEXT_NODE:
  359. return n.data
  360. if n.nodeName == 'computeroutput':
  361. for node in n.childNodes:
  362. text += get_text(node)
  363. return '`{}`'.format(text)
  364. for node in n.childNodes:
  365. if node.nodeType == node.TEXT_NODE:
  366. text += node.data
  367. elif node.nodeType == node.ELEMENT_NODE:
  368. text += get_text(node, preformatted)
  369. return text
  370. # Gets the length of the last line in `text`, excluding newline ("\n") char.
  371. def len_lastline(text):
  372. lastnl = text.rfind('\n')
  373. if -1 == lastnl:
  374. return len(text)
  375. if '\n' == text[-1]:
  376. return lastnl - (1 + text.rfind('\n', 0, lastnl))
  377. return len(text) - (1 + lastnl)
  378. def len_lastline_withoutindent(text, indent):
  379. n = len_lastline(text)
  380. return (n - len(indent)) if n > len(indent) else 0
  381. # Returns True if node `n` contains only inline (not block-level) elements.
  382. def is_inline(n):
  383. # if len(n.childNodes) == 0:
  384. # return n.nodeType == n.TEXT_NODE or n.nodeName == 'computeroutput'
  385. for c in n.childNodes:
  386. if c.nodeType != c.TEXT_NODE and c.nodeName != 'computeroutput':
  387. return False
  388. if not is_inline(c):
  389. return False
  390. return True
  391. def doc_wrap(text, prefix='', width=70, func=False, indent=None):
  392. """Wraps text to `width`.
  393. First line is prefixed with `prefix`, subsequent lines are aligned.
  394. If `func` is True, only wrap at commas.
  395. """
  396. if not width:
  397. # return prefix + text
  398. return text
  399. # Whitespace used to indent all lines except the first line.
  400. indent = ' ' * len(prefix) if indent is None else indent
  401. indent_only = (prefix == '' and indent is not None)
  402. if func:
  403. lines = [prefix]
  404. for part in text.split(', '):
  405. if part[-1] not in ');':
  406. part += ', '
  407. if len(lines[-1]) + len(part) > width:
  408. lines.append(indent)
  409. lines[-1] += part
  410. return '\n'.join(x.rstrip() for x in lines).rstrip()
  411. # XXX: Dummy prefix to force TextWrapper() to wrap the first line.
  412. if indent_only:
  413. prefix = indent
  414. tw = textwrap.TextWrapper(break_long_words=False,
  415. break_on_hyphens=False,
  416. width=width,
  417. initial_indent=prefix,
  418. subsequent_indent=indent)
  419. result = '\n'.join(tw.wrap(text.strip()))
  420. # XXX: Remove the dummy prefix.
  421. if indent_only:
  422. result = result[len(indent):]
  423. return result
  424. def max_name(names):
  425. if len(names) == 0:
  426. return 0
  427. return max(len(name) for name in names)
  428. def update_params_map(parent, ret_map, width=text_width - indentation):
  429. """Updates `ret_map` with name:desc key-value pairs extracted
  430. from Doxygen XML node `parent`.
  431. """
  432. params = collections.OrderedDict()
  433. for node in parent.childNodes:
  434. if node.nodeType == node.TEXT_NODE:
  435. continue
  436. name_node = find_first(node, 'parametername')
  437. if name_node.getAttribute('direction') == 'out':
  438. continue
  439. name = get_text(name_node)
  440. if name in param_exclude:
  441. continue
  442. params[name.strip()] = node
  443. max_name_len = max_name(params.keys()) + 8
  444. # `ret_map` is a name:desc map.
  445. for name, node in params.items():
  446. desc = ''
  447. desc_node = get_child(node, 'parameterdescription')
  448. if desc_node:
  449. desc = fmt_node_as_vimhelp(
  450. desc_node, width=width, indent=(' ' * max_name_len))
  451. ret_map[name] = desc
  452. return ret_map
  453. def render_node(n, text, prefix='', indent='', width=text_width - indentation,
  454. fmt_vimhelp=False):
  455. """Renders a node as Vim help text, recursively traversing all descendants."""
  456. def ind(s):
  457. return s if fmt_vimhelp else ''
  458. text = ''
  459. # space_preceding = (len(text) > 0 and ' ' == text[-1][-1])
  460. # text += (int(not space_preceding) * ' ')
  461. if n.nodeName == 'preformatted':
  462. o = get_text(n, preformatted=True)
  463. ensure_nl = '' if o[-1] == '\n' else '\n'
  464. if o[0:4] == 'lua\n':
  465. text += '>lua{}{}\n<'.format(ensure_nl, o[3:-1])
  466. elif o[0:4] == 'vim\n':
  467. text += '>vim{}{}\n<'.format(ensure_nl, o[3:-1])
  468. else:
  469. text += '>{}{}\n<'.format(ensure_nl, o)
  470. elif is_inline(n):
  471. text = doc_wrap(get_text(n), prefix=prefix, indent=indent, width=width)
  472. elif n.nodeName == 'verbatim':
  473. # TODO: currently we don't use this. The "[verbatim]" hint is there as
  474. # a reminder that we must decide how to format this if we do use it.
  475. text += ' [verbatim] {}'.format(get_text(n))
  476. elif n.nodeName == 'listitem':
  477. for c in n.childNodes:
  478. result = render_node(
  479. c,
  480. text,
  481. indent=indent + (' ' * len(prefix)),
  482. width=width
  483. )
  484. if is_blank(result):
  485. continue
  486. text += indent + prefix + result
  487. elif n.nodeName in ('para', 'heading'):
  488. did_prefix = False
  489. for c in n.childNodes:
  490. if (is_inline(c)
  491. and '' != get_text(c).strip()
  492. and text
  493. and ' ' != text[-1]):
  494. text += ' '
  495. text += render_node(c, text, prefix=(prefix if not did_prefix else ''), indent=indent, width=width)
  496. did_prefix = True
  497. elif n.nodeName == 'itemizedlist':
  498. for c in n.childNodes:
  499. text += '{}\n'.format(render_node(c, text, prefix='• ',
  500. indent=indent, width=width))
  501. elif n.nodeName == 'orderedlist':
  502. i = 1
  503. for c in n.childNodes:
  504. if is_blank(get_text(c)):
  505. text += '\n'
  506. continue
  507. text += '{}\n'.format(render_node(c, text, prefix='{}. '.format(i),
  508. indent=indent, width=width))
  509. i = i + 1
  510. elif n.nodeName == 'simplesect' and 'note' == n.getAttribute('kind'):
  511. text += '\nNote:\n '
  512. for c in n.childNodes:
  513. text += render_node(c, text, indent=' ', width=width)
  514. text += '\n'
  515. elif n.nodeName == 'simplesect' and 'warning' == n.getAttribute('kind'):
  516. text += 'Warning:\n '
  517. for c in n.childNodes:
  518. text += render_node(c, text, indent=' ', width=width)
  519. text += '\n'
  520. elif n.nodeName == 'simplesect' and 'see' == n.getAttribute('kind'):
  521. text += ind(' ')
  522. # Example:
  523. # <simplesect kind="see">
  524. # <para>|autocommand|</para>
  525. # </simplesect>
  526. for c in n.childNodes:
  527. text += render_node(c, text, prefix='• ', indent=' ', width=width)
  528. elif n.nodeName == 'simplesect' and 'return' == n.getAttribute('kind'):
  529. text += ind(' ')
  530. for c in n.childNodes:
  531. text += render_node(c, text, indent=' ', width=width)
  532. elif n.nodeName == 'computeroutput':
  533. return get_text(n)
  534. else:
  535. raise RuntimeError('unhandled node type: {}\n{}'.format(
  536. n.nodeName, n.toprettyxml(indent=' ', newl='\n')))
  537. return text
  538. def para_as_map(parent, indent='', width=text_width - indentation, fmt_vimhelp=False):
  539. """Extracts a Doxygen XML <para> node to a map.
  540. Keys:
  541. 'text': Text from this <para> element
  542. 'params': <parameterlist> map
  543. 'return': List of @return strings
  544. 'seealso': List of @see strings
  545. 'xrefs': ?
  546. """
  547. chunks = {
  548. 'text': '',
  549. 'params': collections.OrderedDict(),
  550. 'return': [],
  551. 'seealso': [],
  552. 'xrefs': []
  553. }
  554. # Ordered dict of ordered lists.
  555. groups = collections.OrderedDict([
  556. ('params', []),
  557. ('return', []),
  558. ('seealso', []),
  559. ('xrefs', []),
  560. ])
  561. # Gather nodes into groups. Mostly this is because we want "parameterlist"
  562. # nodes to appear together.
  563. text = ''
  564. kind = ''
  565. last = ''
  566. if is_inline(parent):
  567. # Flatten inline text from a tree of non-block nodes.
  568. text = doc_wrap(render_node(parent, "", fmt_vimhelp=fmt_vimhelp),
  569. indent=indent, width=width)
  570. else:
  571. prev = None # Previous node
  572. for child in parent.childNodes:
  573. if child.nodeName == 'parameterlist':
  574. groups['params'].append(child)
  575. elif child.nodeName == 'xrefsect':
  576. groups['xrefs'].append(child)
  577. elif child.nodeName == 'simplesect':
  578. last = kind
  579. kind = child.getAttribute('kind')
  580. if kind == 'return' or (kind == 'note' and last == 'return'):
  581. groups['return'].append(child)
  582. elif kind == 'see':
  583. groups['seealso'].append(child)
  584. elif kind in ('note', 'warning'):
  585. text += render_node(child, text, indent=indent,
  586. width=width, fmt_vimhelp=fmt_vimhelp)
  587. else:
  588. raise RuntimeError('unhandled simplesect: {}\n{}'.format(
  589. child.nodeName, child.toprettyxml(indent=' ', newl='\n')))
  590. else:
  591. if (prev is not None
  592. and is_inline(self_or_child(prev))
  593. and is_inline(self_or_child(child))
  594. and '' != get_text(self_or_child(child)).strip()
  595. and text
  596. and ' ' != text[-1]):
  597. text += ' '
  598. text += render_node(child, text, indent=indent, width=width,
  599. fmt_vimhelp=fmt_vimhelp)
  600. prev = child
  601. chunks['text'] += text
  602. # Generate map from the gathered items.
  603. if len(groups['params']) > 0:
  604. for child in groups['params']:
  605. update_params_map(child, ret_map=chunks['params'], width=width)
  606. for child in groups['return']:
  607. chunks['return'].append(render_node(
  608. child, '', indent=indent, width=width, fmt_vimhelp=fmt_vimhelp))
  609. for child in groups['seealso']:
  610. # Example:
  611. # <simplesect kind="see">
  612. # <para>|autocommand|</para>
  613. # </simplesect>
  614. chunks['seealso'].append(render_node(
  615. child, '', indent=indent, width=width, fmt_vimhelp=fmt_vimhelp))
  616. xrefs = set()
  617. for child in groups['xrefs']:
  618. # XXX: Add a space (or any char) to `title` here, otherwise xrefs
  619. # ("Deprecated" section) acts very weird...
  620. title = get_text(get_child(child, 'xreftitle')) + ' '
  621. xrefs.add(title)
  622. xrefdesc = get_text(get_child(child, 'xrefdescription'))
  623. chunks['xrefs'].append(doc_wrap(xrefdesc, prefix='{}: '.format(title),
  624. width=width) + '\n')
  625. return chunks, xrefs
  626. def fmt_node_as_vimhelp(parent, width=text_width - indentation, indent='',
  627. fmt_vimhelp=False):
  628. """Renders (nested) Doxygen <para> nodes as Vim :help text.
  629. NB: Blank lines in a docstring manifest as <para> tags.
  630. """
  631. rendered_blocks = []
  632. def fmt_param_doc(m):
  633. """Renders a params map as Vim :help text."""
  634. max_name_len = max_name(m.keys()) + 4
  635. out = ''
  636. for name, desc in m.items():
  637. name = ' • {}'.format('{{{}}}'.format(name).ljust(max_name_len))
  638. out += '{}{}\n'.format(name, desc)
  639. return out.rstrip()
  640. def has_nonexcluded_params(m):
  641. """Returns true if any of the given params has at least
  642. one non-excluded item."""
  643. if fmt_param_doc(m) != '':
  644. return True
  645. for child in parent.childNodes:
  646. para, _ = para_as_map(child, indent, width, fmt_vimhelp)
  647. # Generate text from the gathered items.
  648. chunks = [para['text']]
  649. if len(para['params']) > 0 and has_nonexcluded_params(para['params']):
  650. chunks.append('\nParameters: ~')
  651. chunks.append(fmt_param_doc(para['params']))
  652. if len(para['return']) > 0:
  653. chunks.append('\nReturn: ~')
  654. for s in para['return']:
  655. chunks.append(s)
  656. if len(para['seealso']) > 0:
  657. chunks.append('\nSee also: ~')
  658. for s in para['seealso']:
  659. chunks.append(s)
  660. for s in para['xrefs']:
  661. chunks.append(s)
  662. rendered_blocks.append(clean_lines('\n'.join(chunks).strip()))
  663. rendered_blocks.append('')
  664. return clean_lines('\n'.join(rendered_blocks).strip())
  665. def extract_from_xml(filename, target, width, fmt_vimhelp):
  666. """Extracts Doxygen info as maps without formatting the text.
  667. Returns two maps:
  668. 1. Functions
  669. 2. Deprecated functions
  670. The `fmt_vimhelp` variable controls some special cases for use by
  671. fmt_doxygen_xml_as_vimhelp(). (TODO: ugly :)
  672. """
  673. fns = {} # Map of func_name:docstring.
  674. deprecated_fns = {} # Map of func_name:docstring.
  675. dom = minidom.parse(filename)
  676. compoundname = get_text(dom.getElementsByTagName('compoundname')[0])
  677. for member in dom.getElementsByTagName('memberdef'):
  678. if member.getAttribute('static') == 'yes' or \
  679. member.getAttribute('kind') != 'function' or \
  680. member.getAttribute('prot') == 'private' or \
  681. get_text(get_child(member, 'name')).startswith('_'):
  682. continue
  683. loc = find_first(member, 'location')
  684. if 'private' in loc.getAttribute('file'):
  685. continue
  686. return_type = get_text(get_child(member, 'type'))
  687. if return_type == '':
  688. continue
  689. if return_type.startswith(('ArrayOf', 'DictionaryOf')):
  690. parts = return_type.strip('_').split('_')
  691. return_type = '{}({})'.format(parts[0], ', '.join(parts[1:]))
  692. name = get_text(get_child(member, 'name'))
  693. annotations = get_text(get_child(member, 'argsstring'))
  694. if annotations and ')' in annotations:
  695. annotations = annotations.rsplit(')', 1)[-1].strip()
  696. # XXX: (doxygen 1.8.11) 'argsstring' only includes attributes of
  697. # non-void functions. Special-case void functions here.
  698. if name == 'nvim_get_mode' and len(annotations) == 0:
  699. annotations += 'FUNC_API_FAST'
  700. annotations = filter(None, map(lambda x: annotation_map.get(x),
  701. annotations.split()))
  702. params = []
  703. type_length = 0
  704. for param in iter_children(member, 'param'):
  705. param_type = get_text(get_child(param, 'type')).strip()
  706. param_name = ''
  707. declname = get_child(param, 'declname')
  708. if declname:
  709. param_name = get_text(declname).strip()
  710. elif CONFIG[target]['mode'] == 'lua':
  711. # XXX: this is what lua2dox gives us...
  712. param_name = param_type
  713. param_type = ''
  714. if param_name in param_exclude:
  715. continue
  716. if fmt_vimhelp and param_type.endswith('*'):
  717. param_type = param_type.strip('* ')
  718. param_name = '*' + param_name
  719. type_length = max(type_length, len(param_type))
  720. params.append((param_type, param_name))
  721. # Handle Object Oriented style functions here.
  722. # We make sure they have "self" in the parameters,
  723. # and a parent function
  724. if return_type.startswith('function') \
  725. and len(return_type.split(' ')) >= 2 \
  726. and any(x[1] == 'self' for x in params):
  727. split_return = return_type.split(' ')
  728. name = f'{split_return[1]}:{name}'
  729. c_args = []
  730. for param_type, param_name in params:
  731. c_args.append((' ' if fmt_vimhelp else '') + (
  732. '%s %s' % (param_type.ljust(type_length), param_name)).strip())
  733. if not fmt_vimhelp:
  734. pass
  735. else:
  736. fstem = '?'
  737. if '.' in compoundname:
  738. fstem = compoundname.split('.')[0]
  739. fstem = CONFIG[target]['module_override'].get(fstem, fstem)
  740. vimtag = CONFIG[target]['fn_helptag_fmt'](fstem, name)
  741. prefix = '%s(' % name
  742. suffix = '%s)' % ', '.join('{%s}' % a[1] for a in params
  743. if a[0] not in ('void', 'Error', 'Arena',
  744. 'lua_State'))
  745. if not fmt_vimhelp:
  746. c_decl = '%s %s(%s);' % (return_type, name, ', '.join(c_args))
  747. signature = prefix + suffix
  748. else:
  749. c_decl = textwrap.indent('%s %s(\n%s\n);' % (return_type, name,
  750. ',\n'.join(c_args)),
  751. ' ')
  752. # Minimum 8 chars between signature and vimtag
  753. lhs = (width - 8) - len(vimtag)
  754. if len(prefix) + len(suffix) > lhs:
  755. signature = vimtag.rjust(width) + '\n'
  756. signature += doc_wrap(suffix, width=width, prefix=prefix,
  757. func=True)
  758. else:
  759. signature = prefix + suffix
  760. signature += vimtag.rjust(width - len(signature))
  761. # Tracks `xrefsect` titles. As of this writing, used only for separating
  762. # deprecated functions.
  763. xrefs_all = set()
  764. paras = []
  765. brief_desc = find_first(member, 'briefdescription')
  766. if brief_desc:
  767. for child in brief_desc.childNodes:
  768. para, xrefs = para_as_map(child)
  769. xrefs_all.update(xrefs)
  770. desc = find_first(member, 'detaileddescription')
  771. if desc:
  772. for child in desc.childNodes:
  773. para, xrefs = para_as_map(child)
  774. paras.append(para)
  775. xrefs_all.update(xrefs)
  776. log.debug(
  777. textwrap.indent(
  778. re.sub(r'\n\s*\n+', '\n',
  779. desc.toprettyxml(indent=' ', newl='\n')),
  780. ' ' * indentation))
  781. fn = {
  782. 'annotations': list(annotations),
  783. 'signature': signature,
  784. 'parameters': params,
  785. 'parameters_doc': collections.OrderedDict(),
  786. 'doc': [],
  787. 'return': [],
  788. 'seealso': [],
  789. }
  790. if fmt_vimhelp:
  791. fn['desc_node'] = desc
  792. fn['brief_desc_node'] = brief_desc
  793. for m in paras:
  794. if 'text' in m:
  795. if not m['text'] == '':
  796. fn['doc'].append(m['text'])
  797. if 'params' in m:
  798. # Merge OrderedDicts.
  799. fn['parameters_doc'].update(m['params'])
  800. if 'return' in m and len(m['return']) > 0:
  801. fn['return'] += m['return']
  802. if 'seealso' in m and len(m['seealso']) > 0:
  803. fn['seealso'] += m['seealso']
  804. if INCLUDE_C_DECL:
  805. fn['c_decl'] = c_decl
  806. if 'Deprecated' in str(xrefs_all):
  807. deprecated_fns[name] = fn
  808. elif name.startswith(CONFIG[target]['fn_name_prefix']):
  809. fns[name] = fn
  810. fns = collections.OrderedDict(sorted(
  811. fns.items(),
  812. key=lambda key_item_tuple: key_item_tuple[0].lower()))
  813. deprecated_fns = collections.OrderedDict(sorted(deprecated_fns.items()))
  814. return fns, deprecated_fns
  815. def fmt_doxygen_xml_as_vimhelp(filename, target):
  816. """Entrypoint for generating Vim :help from from Doxygen XML.
  817. Returns 2 items:
  818. 1. Vim help text for functions found in `filename`.
  819. 2. Vim help text for deprecated functions.
  820. """
  821. fns_txt = {} # Map of func_name:vim-help-text.
  822. deprecated_fns_txt = {} # Map of func_name:vim-help-text.
  823. fns, _ = extract_from_xml(filename, target, text_width, True)
  824. for name, fn in fns.items():
  825. # Generate Vim :help for parameters.
  826. if fn['desc_node']:
  827. doc = fmt_node_as_vimhelp(fn['desc_node'], fmt_vimhelp=True)
  828. if not doc and fn['brief_desc_node']:
  829. doc = fmt_node_as_vimhelp(fn['brief_desc_node'])
  830. if not doc and name.startswith("nvim__"):
  831. continue
  832. if not doc:
  833. doc = 'TODO: Documentation'
  834. annotations = '\n'.join(fn['annotations'])
  835. if annotations:
  836. annotations = ('\n\nAttributes: ~\n' +
  837. textwrap.indent(annotations, ' '))
  838. i = doc.rfind('Parameters: ~')
  839. if i == -1:
  840. doc += annotations
  841. else:
  842. doc = doc[:i] + annotations + '\n\n' + doc[i:]
  843. if INCLUDE_C_DECL:
  844. doc += '\n\nC Declaration: ~\n>\n'
  845. doc += fn['c_decl']
  846. doc += '\n<'
  847. func_doc = fn['signature'] + '\n'
  848. func_doc += textwrap.indent(clean_lines(doc), ' ' * indentation)
  849. # Verbatim handling.
  850. func_doc = re.sub(r'^\s+([<>])$', r'\1', func_doc, flags=re.M)
  851. split_lines = func_doc.split('\n')
  852. start = 0
  853. while True:
  854. try:
  855. start = split_lines.index('>', start)
  856. except ValueError:
  857. break
  858. try:
  859. end = split_lines.index('<', start)
  860. except ValueError:
  861. break
  862. split_lines[start + 1:end] = [
  863. (' ' + x).rstrip()
  864. for x in textwrap.dedent(
  865. "\n".join(
  866. split_lines[start+1:end]
  867. )
  868. ).split("\n")
  869. ]
  870. start = end
  871. func_doc = "\n".join(map(align_tags, split_lines))
  872. if (name.startswith(CONFIG[target]['fn_name_prefix'])
  873. and name != "nvim_error_event"):
  874. fns_txt[name] = func_doc
  875. return ('\n\n'.join(list(fns_txt.values())),
  876. '\n\n'.join(list(deprecated_fns_txt.values())))
  877. def delete_lines_below(filename, tokenstr):
  878. """Deletes all lines below the line containing `tokenstr`, the line itself,
  879. and one line above it.
  880. """
  881. lines = open(filename).readlines()
  882. i = 0
  883. found = False
  884. for i, line in enumerate(lines, 1):
  885. if tokenstr in line:
  886. found = True
  887. break
  888. if not found:
  889. raise RuntimeError(f'not found: "{tokenstr}"')
  890. i = max(0, i - 2)
  891. with open(filename, 'wt') as fp:
  892. fp.writelines(lines[0:i])
  893. def main(doxygen_config, args):
  894. """Generates:
  895. 1. Vim :help docs
  896. 2. *.mpack files for use by API clients
  897. Doxygen is called and configured through stdin.
  898. """
  899. for target in CONFIG:
  900. if args.target is not None and target != args.target:
  901. continue
  902. mpack_file = os.path.join(
  903. base_dir, 'runtime', 'doc',
  904. CONFIG[target]['filename'].replace('.txt', '.mpack'))
  905. if os.path.exists(mpack_file):
  906. os.remove(mpack_file)
  907. output_dir = out_dir.format(target=target)
  908. log.info("Generating documentation for %s in folder %s",
  909. target, output_dir)
  910. debug = args.log_level >= logging.DEBUG
  911. p = subprocess.Popen(
  912. ['doxygen', '-'],
  913. stdin=subprocess.PIPE,
  914. # silence warnings
  915. # runtime/lua/vim/lsp.lua:209: warning: argument 'foo' not found
  916. stderr=(subprocess.STDOUT if debug else subprocess.DEVNULL))
  917. p.communicate(
  918. doxygen_config.format(
  919. input=' '.join(
  920. [f'"{file}"' for file in CONFIG[target]['files']]),
  921. output=output_dir,
  922. filter=filter_cmd,
  923. file_patterns=CONFIG[target]['file_patterns'])
  924. .encode('utf8')
  925. )
  926. if p.returncode:
  927. sys.exit(p.returncode)
  928. fn_map_full = {} # Collects all functions as each module is processed.
  929. sections = {}
  930. section_docs = {}
  931. sep = '=' * text_width
  932. base = os.path.join(output_dir, 'xml')
  933. dom = minidom.parse(os.path.join(base, 'index.xml'))
  934. # Generate module-level (section) docs (@defgroup).
  935. for compound in dom.getElementsByTagName('compound'):
  936. if compound.getAttribute('kind') != 'group':
  937. continue
  938. # Doxygen "@defgroup" directive.
  939. groupname = get_text(find_first(compound, 'name'))
  940. groupxml = os.path.join(base, '%s.xml' %
  941. compound.getAttribute('refid'))
  942. group_parsed = minidom.parse(groupxml)
  943. doc_list = []
  944. brief_desc = find_first(group_parsed, 'briefdescription')
  945. if brief_desc:
  946. for child in brief_desc.childNodes:
  947. doc_list.append(fmt_node_as_vimhelp(child))
  948. desc = find_first(group_parsed, 'detaileddescription')
  949. if desc:
  950. doc = fmt_node_as_vimhelp(desc)
  951. if doc:
  952. doc_list.append(doc)
  953. section_docs[groupname] = "\n".join(doc_list)
  954. # Generate docs for all functions in the current module.
  955. for compound in dom.getElementsByTagName('compound'):
  956. if compound.getAttribute('kind') != 'file':
  957. continue
  958. filename = get_text(find_first(compound, 'name'))
  959. if filename.endswith('.c') or filename.endswith('.lua'):
  960. xmlfile = os.path.join(base, '{}.xml'.format(compound.getAttribute('refid')))
  961. # Extract unformatted (*.mpack).
  962. fn_map, _ = extract_from_xml(xmlfile, target, 9999, False)
  963. # Extract formatted (:help).
  964. functions_text, deprecated_text = fmt_doxygen_xml_as_vimhelp(
  965. os.path.join(base, '{}.xml'.format(compound.getAttribute('refid'))), target)
  966. if not functions_text and not deprecated_text:
  967. continue
  968. else:
  969. filename = os.path.basename(filename)
  970. name = os.path.splitext(filename)[0].lower()
  971. sectname = name.upper() if name == 'ui' else name.title()
  972. sectname = CONFIG[target]['section_name'].get(filename, sectname)
  973. title = CONFIG[target]['section_fmt'](sectname)
  974. section_tag = CONFIG[target]['helptag_fmt'](sectname)
  975. # Module/Section id matched against @defgroup.
  976. # "*api-buffer*" => "api-buffer"
  977. section_id = section_tag.strip('*')
  978. doc = ''
  979. section_doc = section_docs.get(section_id)
  980. if section_doc:
  981. doc += '\n\n' + section_doc
  982. if functions_text:
  983. doc += '\n\n' + functions_text
  984. if INCLUDE_DEPRECATED and deprecated_text:
  985. doc += f'\n\n\nDeprecated {sectname} Functions: ~\n\n'
  986. doc += deprecated_text
  987. if doc:
  988. sections[filename] = (title, section_tag, doc)
  989. fn_map_full.update(fn_map)
  990. if len(sections) == 0:
  991. fail(f'no sections for target: {target} (look for errors near "Preprocessing" log lines above)')
  992. if len(sections) > len(CONFIG[target]['section_order']):
  993. raise RuntimeError(
  994. 'found new modules "{}"; update the "section_order" map'.format(
  995. set(sections).difference(CONFIG[target]['section_order'])))
  996. first_section_tag = sections[CONFIG[target]['section_order'][0]][1]
  997. docs = ''
  998. for filename in CONFIG[target]['section_order']:
  999. try:
  1000. title, section_tag, section_doc = sections.pop(filename)
  1001. except KeyError:
  1002. msg(f'warning: empty docs, skipping (target={target}): {filename}')
  1003. msg(f' existing docs: {sections.keys()}')
  1004. continue
  1005. if filename not in CONFIG[target]['append_only']:
  1006. docs += sep
  1007. docs += '\n{}{}'.format(title, section_tag.rjust(text_width - len(title)))
  1008. docs += section_doc
  1009. docs += '\n\n\n'
  1010. docs = docs.rstrip() + '\n\n'
  1011. docs += f' vim:tw=78:ts=8:sw={indentation}:sts={indentation}:et:ft=help:norl:\n'
  1012. doc_file = os.path.join(base_dir, 'runtime', 'doc',
  1013. CONFIG[target]['filename'])
  1014. if os.path.exists(doc_file):
  1015. delete_lines_below(doc_file, first_section_tag)
  1016. with open(doc_file, 'ab') as fp:
  1017. fp.write(docs.encode('utf8'))
  1018. fn_map_full = collections.OrderedDict(sorted(fn_map_full.items()))
  1019. with open(mpack_file, 'wb') as fp:
  1020. fp.write(msgpack.packb(fn_map_full, use_bin_type=True))
  1021. if not args.keep_tmpfiles:
  1022. shutil.rmtree(output_dir)
  1023. msg_report()
  1024. def filter_source(filename, keep_tmpfiles):
  1025. output_dir = out_dir.format(target='lua2dox')
  1026. name, extension = os.path.splitext(filename)
  1027. if extension == '.lua':
  1028. args = [str(nvim), '-l', lua2dox, filename] + (['--outdir', output_dir] if keep_tmpfiles else [])
  1029. p = subprocess.run(args, stdout=subprocess.PIPE)
  1030. op = ('?' if 0 != p.returncode else p.stdout.decode('utf-8'))
  1031. print(op)
  1032. else:
  1033. """Filters the source to fix macros that confuse Doxygen."""
  1034. with open(filename, 'rt') as fp:
  1035. print(re.sub(r'^(ArrayOf|DictionaryOf)(\(.*?\))',
  1036. lambda m: m.group(1)+'_'.join(
  1037. re.split(r'[^\w]+', m.group(2))),
  1038. fp.read(), flags=re.M))
  1039. def parse_args():
  1040. targets = ', '.join(CONFIG.keys())
  1041. ap = argparse.ArgumentParser(
  1042. description="Generate helpdoc from source code")
  1043. ap.add_argument(
  1044. "--log-level", "-l", choices=LOG_LEVELS.keys(),
  1045. default=logging.getLevelName(logging.ERROR), help="Set log verbosity"
  1046. )
  1047. ap.add_argument('source_filter', nargs='*',
  1048. help="Filter source file(s)")
  1049. ap.add_argument('-k', '--keep-tmpfiles', action='store_true',
  1050. help="Keep temporary files (tmp-xx-doc/ directories, including tmp-lua2dox-doc/ for lua2dox.lua quasi-C output)")
  1051. ap.add_argument('-t', '--target',
  1052. help=f'One of ({targets}), defaults to "all"')
  1053. return ap.parse_args()
  1054. Doxyfile = textwrap.dedent('''
  1055. OUTPUT_DIRECTORY = {output}
  1056. INPUT = {input}
  1057. INPUT_ENCODING = UTF-8
  1058. FILE_PATTERNS = {file_patterns}
  1059. RECURSIVE = YES
  1060. INPUT_FILTER = "{filter}"
  1061. EXCLUDE =
  1062. EXCLUDE_SYMLINKS = NO
  1063. EXCLUDE_PATTERNS = */private/* */health.lua */_*.lua
  1064. EXCLUDE_SYMBOLS =
  1065. EXTENSION_MAPPING = lua=C
  1066. EXTRACT_PRIVATE = NO
  1067. GENERATE_HTML = NO
  1068. GENERATE_DOCSET = NO
  1069. GENERATE_HTMLHELP = NO
  1070. GENERATE_QHP = NO
  1071. GENERATE_TREEVIEW = NO
  1072. GENERATE_LATEX = NO
  1073. GENERATE_RTF = NO
  1074. GENERATE_MAN = NO
  1075. GENERATE_DOCBOOK = NO
  1076. GENERATE_AUTOGEN_DEF = NO
  1077. GENERATE_XML = YES
  1078. XML_OUTPUT = xml
  1079. XML_PROGRAMLISTING = NO
  1080. ENABLE_PREPROCESSING = YES
  1081. MACRO_EXPANSION = YES
  1082. EXPAND_ONLY_PREDEF = NO
  1083. MARKDOWN_SUPPORT = YES
  1084. ''')
  1085. if __name__ == "__main__":
  1086. args = parse_args()
  1087. print("Setting log level to %s" % args.log_level)
  1088. args.log_level = LOG_LEVELS[args.log_level]
  1089. log.setLevel(args.log_level)
  1090. log.addHandler(logging.StreamHandler())
  1091. # When invoked as a filter, args won't be passed, so use an env var.
  1092. if args.keep_tmpfiles:
  1093. os.environ['NVIM_KEEP_TMPFILES'] = '1'
  1094. keep_tmpfiles = ('NVIM_KEEP_TMPFILES' in os.environ)
  1095. if len(args.source_filter) > 0:
  1096. filter_source(args.source_filter[0], keep_tmpfiles)
  1097. else:
  1098. main(Doxyfile, args)
  1099. # vim: set ft=python ts=4 sw=4 tw=79 et :