3 # gtk-doc - GTK DocBook documentation generator.
4 # Copyright (C) 1998 Damon Chaplin
5 # 2007-2016 Stefan Sauer
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 ''"Fix cross-references in the HTML documentation.''"
24 # Support both Python 2 and 3
25 from __future__
import print_function
35 from . import common
, config
37 # This contains all the entities and their relative URLs.
40 # failing link targets we don't warn about even once
58 # Cache of dirs we already scanned for index files
64 m
= re
.search(r
'(.*?)/share/gtk-doc/html', options
.html_dir
)
66 path_prefix
= m
.group(1)
67 logging
.info('Path prefix: %s', path_prefix
)
68 prefix_match
= r
'^' + re
.escape(path_prefix
) + r
'/'
70 # We scan the directory containing GLib and any directories in GNOME2_PATH
71 # first, but these will be overriden by any later scans.
72 dir = common
.GetModuleDocDir('glib-2.0')
73 if dir and os
.path
.exists(dir):
74 # Some predefined link targets to get links into type hierarchies as these
75 # have no targets. These are always absolute for now.
76 Links
['GBoxed'] = dir + '/gobject/gobject-Boxed-Types.html'
77 Links
['GEnum'] = dir + '/gobject/gobject-Enumeration-and-Flag-Types.html'
78 Links
['GFlags'] = dir + '/gobject/gobject-Enumeration-and-Flag-Types.html'
79 Links
['GInterface'] = dir + '/gobject/GTypeModule.html'
81 if dir != options
.html_dir
:
82 logging
.info('Scanning GLib directory: %s', dir)
83 ScanIndices(dir, (re
.search(prefix_match
, dir) is None))
85 path
= os
.environ
.get('GNOME2_PATH')
87 for dir in path
.split(':'):
88 dir += 'share/gtk-doc/html'
89 if os
.path
.exists(dir) and dir != options
.html_dir
:
90 logging
.info('Scanning GNOME2_PATH directory: %s', dir)
91 ScanIndices(dir, (re
.search(prefix_match
, dir) is None))
93 logging
.info('Scanning HTML_DIR directory: %s', options
.html_dir
)
94 ScanIndices(options
.html_dir
, 0)
95 logging
.info('Scanning MODULE_DIR directory: %s', options
.module_dir
)
96 ScanIndices(options
.module_dir
, 0)
98 # check all extra dirs, but skip already scanned dirs or subdirs of those
99 for dir in options
.extra_dir
:
100 dir = dir.rstrip('/')
101 logging
.info('Scanning EXTRA_DIR directory: %s', dir)
103 # If the --extra-dir option is not relative and is not sharing the same
104 # prefix as the target directory of the docs, we need to use absolute
105 # directories for the links
106 if not dir.startswith('..') and re
.search(prefix_match
, dir) is None:
111 ReadSections(options
)
112 FixCrossReferences(options
)
115 def ScanIndices(scan_dir
, use_absolute_links
):
116 if not scan_dir
or scan_dir
in DirCache
:
118 DirCache
[scan_dir
] = 1
120 logging
.info('Scanning index directory: %s, absolute: %d', scan_dir
, use_absolute_links
)
122 # TODO(ensonic): this code is the same as in rebase.py
123 if not os
.path
.isdir(scan_dir
):
124 logging
.info('Cannot open dir "%s"', scan_dir
)
128 for entry
in sorted(os
.listdir(scan_dir
)):
129 full_entry
= os
.path
.join(scan_dir
, entry
)
130 if os
.path
.isdir(full_entry
):
131 subdirs
.append(full_entry
)
134 if entry
.endswith('.devhelp2'):
135 # if devhelp-file is good don't read index.sgml
136 ReadDevhelp(full_entry
, use_absolute_links
)
137 elif entry
== "index.sgml.gz" and not os
.path
.exists(os
.path
.join(scan_dir
, 'index.sgml')):
138 # debian/ubuntu started to compress this as index.sgml.gz :/
139 print(''' Please fix https://bugs.launchpad.net/ubuntu/+source/gtk-doc/+bug/77138 . For now run:
142 elif entry
.endswith('.devhelp2.gz') and not os
.path
.exists(full_entry
[:-3]):
143 # debian/ubuntu started to compress this as *devhelp2.gz :/
144 print('''Please fix https://bugs.launchpad.net/ubuntu/+source/gtk-doc/+bug/1466210 . For now run:
147 # we could consider supporting: gzip module
149 # Now recursively scan the subdirectories.
150 for subdir
in subdirs
:
151 ScanIndices(subdir
, use_absolute_links
)
154 def ReadDevhelp(file, use_absolute_links
):
155 # Determine the absolute directory, to be added to links in $file
156 # if we need to use an absolute link.
157 # $file will be something like /prefix/gnome/share/gtk-doc/html/gtk/$file
158 # We want the part up to 'html/.*' since the links in $file include
161 if use_absolute_links
:
162 # For uninstalled index files we'd need to map the path to where it
163 # will be installed to
164 if not file.startswith('./'):
165 m
= re
.search(r
'(.*\/)(.*?)\/.*?\.devhelp2', file)
166 dir = m
.group(1) + m
.group(2) + '/'
168 m
= re
.search(r
'(.*\/)(.*?)\/.*?\.devhelp2', file)
170 dir += m
.group(2) + '/'
174 logging
.info('Scanning index file=%s, absolute=%d, dir=%s', file, use_absolute_links
, dir)
176 for line
in common
.open_text(file):
177 m
= re
.search(r
' link="([^#]*)#([^"]*)"', line
)
179 link
= m
.group(1) + '#' + m
.group(2)
180 logging
.debug('Found id: %s href: %s', m
.group(2), link
)
181 Links
[m
.group(2)] = dir + link
184 def ReadSections(options
):
185 for line
in common
.open_text(options
.module
+ '-sections.txt'):
186 m1
= re
.search(r
'^<SUBSECTION\s*(.*)>', line
)
187 if line
.startswith('#') or line
.strip() == '':
189 elif line
.startswith('<SECTION>'):
192 subsection
= m1
.group(1)
193 elif line
.startswith('<SUBSECTION>') or line
.startswith('</SECTION>'):
195 elif re
.search(r
'^<TITLE>(.*)<\/TITLE>', line
):
197 elif re
.search(r
'^<FILE>(.*)<\/FILE>', line
):
199 elif re
.search(r
'^<INCLUDE>(.*)<\/INCLUDE>', line
):
202 symbol
= line
.strip()
203 if subsection
== "Standard" or subsection
== "Private":
204 NoLinks
.add(common
.CreateValidSGMLID(symbol
))
207 def FixCrossReferences(options
):
208 scan_dir
= options
.module_dir
209 # TODO(ensonic): use glob.glob()?
210 for entry
in sorted(os
.listdir(scan_dir
)):
211 full_entry
= os
.path
.join(scan_dir
, entry
)
212 if os
.path
.isdir(full_entry
):
214 elif entry
.endswith('.html') or entry
.endswith('.htm'):
215 FixHTMLFile(options
, full_entry
)
218 def FixHTMLFile(options
, file):
219 logging
.info('Fixing file: %s', file)
221 content
= common
.open_text(file).read()
224 # FIXME: ideally we'd pass a clue about the example language to the highligher
225 # unfortunately the "language" attribute is not appearing in the html output
226 # we could patch the customization to have <code class="xxx"> inside of <pre>
227 if config
.highlight
.endswith('vim'):
229 return HighlightSourceVim(options
, m
.group(1), m
.group(2))
231 r
'<div class=\"(example-contents|informalexample)\"><pre class=\"programlisting\">(.*?)</pre></div>',
232 repl_func
, content
, flags
=re
.DOTALL
)
235 return HighlightSource(options
, m
.group(1), m
.group(2))
237 r
'<div class=\"(example-contents|informalexample)\"><pre class=\"programlisting\">(.*?)</pre></div>',
238 repl_func
, content
, flags
=re
.DOTALL
)
240 content
= re
.sub(r
'\<GTKDOCLINK\s+HREF=\"(.*?)\"\>(.*?)\</GTKDOCLINK\>',
241 r
'\<GTKDOCLINK\ HREF=\"\1\"\>\2\</GTKDOCLINK\>', content
, flags
=re
.DOTALL
)
243 # From the highlighter we get all the functions marked up. Now we can turn them into GTKDOCLINK items
245 return MakeGtkDocLink(m
.group(1), m
.group(2), m
.group(3))
246 content
= re
.sub(r
'(<span class=\"function\">)(.*?)(</span>)', repl_func
, content
, flags
=re
.DOTALL
)
247 # We can also try the first item in stuff marked up as 'normal'
249 r
'(<span class=\"normal\">\s*)(.+?)((\s+.+?)?\s*</span>)', repl_func
, content
, flags
=re
.DOTALL
)
251 lines
= content
.rstrip().split('\n')
253 def repl_func_with_ix(i
):
255 return MakeXRef(options
, file, i
+ 1, m
.group(1), m
.group(2))
258 for i
in range(len(lines
)):
259 lines
[i
] = re
.sub(r
'<GTKDOCLINK\s+HREF="([^"]*)"\s*>(.*?)</GTKDOCLINK\s*>', repl_func_with_ix(i
), lines
[i
])
260 if 'GTKDOCLINK' in lines
[i
]:
261 logging
.info('make xref failed for line %d: "%s"', i
, lines
[i
])
263 new_file
= file + '.new'
264 content
= '\n'.join(lines
)
265 with common
.open_text(new_file
, 'w') as h
:
269 os
.rename(new_file
, file)
272 def MakeXRef(options
, file, line
, id, text
):
275 # This is a workaround for some inconsistency we have with CreateValidSGMLID
276 if not href
and ':' in id:
277 href
= Links
.get(id.replace(':', '--'))
278 # poor mans plural support
279 if not href
and id.endswith('s'):
281 href
= Links
.get(tid
)
283 href
= Links
.get(tid
+ '-struct')
285 href
= Links
.get(id + '-struct')
288 # if it is a link to same module, remove path to make it work uninstalled
289 m
= re
.search(r
'^\.\./' + options
.module
+ '/(.*)$', href
)
292 logging
.info('Fixing link to uninstalled doc: %s, %s, %s', id, href
, text
)
294 logging
.info('Fixing link: %s, %s, %s', id, href
, text
)
295 return "<a href=\"%s\">%s</a>" % (href
, text
)
297 logging
.info('no link for: %s, %s', id, text
)
299 # don't warn multiple times and also skip blacklisted (ctypes)
302 # if it's a function, don't warn if it does not contain a "_"
303 # (transformed to "-")
304 # - gnome coding style would use '_'
305 # - will avoid wrong warnings for ansi c functions
306 if re
.search(r
' class=\"function\"', text
) and '-' not in id:
308 # if it's a 'return value', don't warn (implicitly created link)
309 if re
.search(r
' class=\"returnvalue\"', text
):
311 # if it's a 'type', don't warn if it starts with lowercase
312 # - gnome coding style would use CamelCase
313 if re
.search(r
' class=\"type\"', text
) and id[0].islower():
315 # don't warn for self links
319 common
.LogWarning(file, line
, 'no link for: "%s" -> (%s).' % (id, text
))
324 def MakeGtkDocLink(pre
, symbol
, post
):
325 id = common
.CreateValidSGMLID(symbol
)
327 # these are implicitely created links in highlighed sources
328 # we don't want warnings for those if the links cannot be resolved.
331 return pre
+ '<GTKDOCLINK HREF="' + id + '">' + symbol
+ '</GTKDOCLINK>' + post
334 def HighlightSource(options
, type, source
):
335 # write source to a temp file
336 # FIXME: use .c for now to hint the language to the highlighter
337 with tempfile
.NamedTemporaryFile(mode
='w+', suffix
='.c') as f
:
338 temp_source_file
= HighlightSourcePreProcess(f
, source
)
339 highlight_options
= config
.highlight_options
.replace('$SRC_LANG', options
.src_lang
)
341 logging
.info('running %s %s %s', config
.highlight
, highlight_options
, temp_source_file
)
344 highlighted_source
= subprocess
.check_output(
345 [config
.highlight
] + shlex
.split(highlight_options
) + [temp_source_file
]).decode('utf-8')
346 logging
.debug('result: [%s]', highlighted_source
)
347 if config
.highlight
.endswith('/source-highlight'):
348 highlighted_source
= re
.sub(r
'^<\!-- .*? -->', '', highlighted_source
, flags
=re
.MULTILINE | re
.DOTALL
)
349 highlighted_source
= re
.sub(
350 r
'<pre><tt>(.*?)</tt></pre>', r
'\1', highlighted_source
, flags
=re
.MULTILINE | re
.DOTALL
)
351 elif config
.highlight
.endswith('/highlight'):
352 # need to rewrite the stylesheet classes
353 highlighted_source
= highlighted_source
.replace('<span class="gtkdoc com">', '<span class="comment">')
354 highlighted_source
= highlighted_source
.replace('<span class="gtkdoc dir">', '<span class="preproc">')
355 highlighted_source
= highlighted_source
.replace('<span class="gtkdoc kwd">', '<span class="function">')
356 highlighted_source
= highlighted_source
.replace('<span class="gtkdoc kwa">', '<span class="keyword">')
357 highlighted_source
= highlighted_source
.replace('<span class="gtkdoc line">', '<span class="linenum">')
358 highlighted_source
= highlighted_source
.replace('<span class="gtkdoc num">', '<span class="number">')
359 highlighted_source
= highlighted_source
.replace('<span class="gtkdoc str">', '<span class="string">')
360 highlighted_source
= highlighted_source
.replace('<span class="gtkdoc sym">', '<span class="symbol">')
362 # highlighted_source = re.sub(r'</span>(.+)<span', '</span><span class="normal">\1</span><span')
364 return HighlightSourcePostprocess(type, highlighted_source
)
367 def HighlightSourceVim(options
, type, source
):
368 # write source to a temp file
369 with tempfile
.NamedTemporaryFile(mode
='w+', suffix
='.h') as f
:
370 temp_source_file
= HighlightSourcePreProcess(f
, source
)
373 # TODO(ensonic): use p.communicate()
374 script
= "echo 'let html_number_lines=0|let html_use_css=1|let html_use_xhtml=1|e %s|syn on|set syntax=%s|run! plugin/tohtml.vim|run! syntax/2html.vim|w! %s.html|qa!' | " % (
375 temp_source_file
, options
.src_lang
, temp_source_file
)
376 script
+= "%s -n -e -u NONE -T xterm >/dev/null" % config
.highlight
377 subprocess
.check_call([script
], shell
=True)
379 highlighted_source
= common
.open_text(temp_source_file
+ ".html").read()
380 highlighted_source
= re
.sub(r
'.*<pre\b[^>]*>\n', '', highlighted_source
, flags
=re
.DOTALL
)
381 highlighted_source
= re
.sub(r
'</pre>.*', '', highlighted_source
, flags
=re
.DOTALL
)
383 # need to rewrite the stylesheet classes
384 highlighted_source
= highlighted_source
.replace('<span class="Comment">', '<span class="comment">')
385 highlighted_source
= highlighted_source
.replace('<span class="PreProc">', '<span class="preproc">')
386 highlighted_source
= highlighted_source
.replace('<span class="Statement">', '<span class="keyword">')
387 highlighted_source
= highlighted_source
.replace('<span class="Identifier">', '<span class="function">')
388 highlighted_source
= highlighted_source
.replace('<span class="Constant">', '<span class="number">')
389 highlighted_source
= highlighted_source
.replace('<span class="Special">', '<span class="symbol">')
390 highlighted_source
= highlighted_source
.replace('<span class="Type">', '<span class="type">')
393 os
.unlink(temp_source_file
+ '.html')
395 return HighlightSourcePostprocess(type, highlighted_source
)
398 def HighlightSourcePreProcess(f
, source
):
399 # chop of leading and trailing empty lines, leave leading space in first real line
400 source
= source
.strip(' ')
401 source
= source
.strip('\n')
402 source
= source
.rstrip()
405 m
= re
.search(r
'^(\s+)', source
)
407 source
= re
.sub(r
'^' + m
.group(1), '', source
, flags
=re
.MULTILINE
)
408 # avoid double entity replacement
409 source
= source
.replace('<', '<')
410 source
= source
.replace('>', '>')
411 source
= source
.replace('&', '&')
412 if sys
.version_info
< (3,):
413 source
= source
.encode('utf-8')
419 def HighlightSourcePostprocess(type, highlighted_source
):
420 # chop of leading and trailing empty lines
421 highlighted_source
= highlighted_source
.strip()
423 # turn common urls in comments into links
424 highlighted_source
= re
.sub(r
'<span class="url">(.*?)</span>',
425 r
'<span class="url"><a href="\1">\1</a></span>',
426 highlighted_source
, flags
=re
.DOTALL
)
428 # we do own line-numbering
429 line_count
= highlighted_source
.count('\n')
430 source_lines
= '\n'.join([str(i
) for i
in range(1, line_count
+ 2)])
432 return """<div class="%s">
433 <table class="listing_frame" border="0" cellpadding="0" cellspacing="0">
436 <td class="listing_lines" align="right"><pre>%s</pre></td>
437 <td class="listing_code"><pre class="programlisting">%s</pre></td>
442 """ % (type, source_lines
, highlighted_source
)