ENH: Complete re-write of fixupDoxygen.py.in
[freefoam.git] / doc / Doxygen / fixupDoxygen.py.in
blobf0b3b838c71a761691ff3ac207a23291d2a00f06
1 #!@PYTHON_EXECUTABLE@
2 import sys
3 import re
4 import glob
5 import os.path as op
6 import time
7 import md5
8 sys.path.insert(0, '@CMAKE_BINARY_DIR@/data/python')
9 from FreeFOAM.compat import *
11 echo('Fixing up Doxygen generated API documentation')
13 srcdir = '@PROJECT_SOURCE_DIR@'
15 # escaping table
16 # actually, there are many more characters to escape, but those
17 # should be the only ones relevant for FreeFOAM
18 escapeTable = (
19 ('_', '__'),
20 ('/', '_2'),
21 ('.', '_8'),
24 # regular expression to match includes
25 incRegex = re.compile(
26 r'(?P<ptxt>.*<span\s+class="preprocessor">\s*\s*#\s*include\s+' +
27 r'(?:(?P<qinc>&quot;)|(?P<sinc>&lt;)))(?P<hdr>\S+.[CH])' +
28 r'(?P<atxt>(?(qinc)&quot;|&gt;).*)')
30 glob_pattern = op.join(
31 '@CMAKE_CURRENT_BINARY_DIR@', '..', 'html', 'API',
32 '*_source.html')
34 class FilePathParser(HTMLParser.HTMLParser):
35 '''Parse file name and includes from Doxygen-generated documentation'''
36 def __init__(self):
37 HTMLParser.HTMLParser.__init__(self)
38 self.inNavPath = False
39 self.inHeaderTitle = False
40 self._path = ''
41 self._includes = []
43 def parse(self, fname):
44 '''Parse the HTML file `fname`'''
45 lines = open(fname, 'rt').readlines()
46 self.feed(''.join(lines))
47 self.close()
48 # can't use HTMLParse for includes because of the discared white space
49 for l in lines:
50 m = incRegex.search(l)
51 if m is not None:
52 self._includes.append(m)
53 return op.normpath(self._path), self._includes
55 def handle_starttag(self, tag, attrs):
56 if tag == 'div':
57 if ('class', 'navpath') in attrs:
58 self.inNavPath = True
59 elif ('class', 'headertitle') in attrs:
60 self.inHeaderTitle = True
62 def handle_endtag(self, tag):
63 if tag == 'div':
64 if self.inNavPath:
65 self.inNavPath = False
66 elif self.inHeaderTitle:
67 self.inHeaderTitle = False
69 def handle_data(self, data):
70 data = data.strip()
71 if len(data):
72 if self.inNavPath or self.inHeaderTitle:
73 self._path = op.join(self._path, data)
75 def parse_wrapper(hdr):
76 incdir = op.join('@CMAKE_BINARY_DIR@', 'include')
77 wrapper = op.normpath(op.join(srcdir, incdir, hdr))
78 realHdr = None
79 if op.isfile(wrapper):
80 # parse the file for the first #include statement and extract
81 # the file path
82 for ll in open(wrapper, 'rt'):
83 mm = re.match(r'#include "(.*)"', ll)
84 if mm:
85 realHdr = mm.group(1)
86 break
87 if not realHdr:
88 echo('ERROR: Failed to parse include wrapper "%s"'%wrapper,
89 file=sys.stderr)
90 sys.exit(1)
91 # construct the absolute path to the header
92 realHdr = op.normpath(
93 op.join(incdir, op.dirname(hdr), realHdr))
94 if not op.isfile(realHdr):
95 echo('ERROR: Wrapped include file "%s" does not exist'%realHdr,
96 file=sys.stderr)
97 sys.exit(1)
98 return op.relpath(realHdr, srcdir)
100 htmlDocMap = {}
101 sysHdrMap = {}
102 quoteHdrMap = {}
103 echo('- Parsing *_source.html')
104 for f in glob.glob(glob_pattern):
105 parser = FilePathParser()
106 p, incs = parser.parse(f)
107 htmlDocMap[f] = p
108 for i in incs:
109 sysinc = i.group('sinc')!=None
110 hdr = i.group('hdr')
111 if sysinc:
112 if hdr not in sysHdrMap:
113 realHdr = parse_wrapper(hdr)
114 sysHdrMap[hdr] = realHdr
115 else:
116 if p not in quoteHdrMap:
117 quoteHdrMap[p] = {}
118 if hdr not in quoteHdrMap[p]:
119 realHdr = op.normpath(op.join(op.dirname(p), hdr))
120 if not op.isfile(op.join(srcdir, realHdr)):
121 echo('ERROR: Failed to find include file "%s" in %s'%(hdr, p),
122 file=sys.stderr)
123 sys.exit(1)
124 quoteHdrMap[p][hdr] = realHdr
126 # find shortest unique partial paths and mimick the name-mangling used by
127 # doxygen
128 echo('- Computing mangled file names')
129 sources = set(htmlDocMap.itervalues())
130 docMap = dict(zip(sources, sources))
131 for b in map(op.basename, docMap.itervalues()):
132 subDocMap = dict(
133 filter(lambda hd: op.basename(hd[0])==b, docMap.iteritems()))
134 if len(subDocMap) == 1:
135 prefix = op.dirname(subDocMap.values()[0])+op.sep
136 else:
137 prefix = op.commonprefix(subDocMap.values())
138 l = len(prefix)
139 for h, d in subDocMap.iteritems():
140 d = d[l:]
141 for o, n in escapeTable:
142 d = d.replace(o, n)
143 # mimick file name truncation of Doxygen
144 if len(d) >= 128:
145 d = d[:128-32] + md5.new(d).hexdigest()
146 docMap[h] = d
148 # now do the actual work of fixing up the docs
149 echo('- Fixing #include linkes in the *_source.html files')
150 for f, p in htmlDocMap.iteritems():
151 lines = open(f, 'rt').readlines()
152 for i, l in enumerate(lines):
153 m = incRegex.match(l)
154 if m is not None:
155 hdr = m.group('hdr')
156 sysinc = m.group('sinc')!=None
157 if sysinc:
158 realHdr = sysHdrMap[hdr]
159 else:
160 realHdr = quoteHdrMap[p][hdr]
161 lines[i] = ''.join((
162 m.group('ptxt'),
163 '<a class="code" href="%s.html">%s</a>'%(docMap[realHdr], hdr),
164 m.group('atxt'),
165 '\n'))
166 open(f, 'wt').writelines(lines)
168 # replace #CONFIG_YEAR# and #CONFIG_DATE_STAMP#, delete
169 # #include <FOAM_LOCAL/hdr.H> and make #include "src/../hdr.H" links
170 echo('- Fixing remaining #include links and expanding placeholders')
171 year = time.strftime('%Y')
172 stamp = time.strftime('%a, %d %b %Y %H:%M:%S %Z')
173 for f in glob.glob(op.join(
174 '@CMAKE_CURRENT_BINARY_DIR@', '..', 'html', 'API', '*.html')):
175 lines = open(f, 'rt').readlines()
176 for i, l in enumerate(lines):
177 lines[i] = lines[i].replace('#CONFIG_YEAR#', year)
178 lines[i] = lines[i].replace('#CONFIG_DATE_STAMP#', stamp)
179 if re.match(r'^\s*<p><code>#include &lt.*>FOAM_LOCAL/', lines[i]):
180 del lines[i]
181 continue
182 m = re.match(r'(\s*<code>#include &quot;)((?:src|applications)/\S+\.H)' +
183 r'(&quot;</code>.*)', lines[i])
184 if m is not None:
185 hdr = m.group(2)
186 if hdr in docMap:
187 html = docMap[hdr]
188 lines[i] = ''.join((
189 m.group(1),
190 '<a href="%s.html">%s</a>'%(html, hdr),
191 m.group(3)))
192 else:
193 echo('WARNING: Failed to find HTML file for "%s"'%hdr,
194 file=sys.stderr)
195 open(f, 'wt').writelines(lines)