link_roles.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. # based on http://protips.readthedocs.io/link-roles.html
  2. from __future__ import print_function
  3. from __future__ import unicode_literals
  4. import re
  5. import os
  6. import subprocess
  7. from docutils import nodes
  8. def get_github_rev():
  9. path = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip().decode('utf-8')
  10. try:
  11. tag = subprocess.check_output(['git', 'describe', '--exact-match']).strip().decode('utf-8')
  12. except subprocess.CalledProcessError:
  13. tag = None
  14. print('Git commit ID: ', path)
  15. if tag:
  16. print('Git tag: ', tag)
  17. return tag
  18. return path
  19. def url_join(*url_parts):
  20. """ Make a URL out of multiple components, assume first part is the https:// part and
  21. anything else is a path component """
  22. result = "/".join(url_parts)
  23. result = re.sub(r"([^:])//+", r"\1/", result) # remove any // that isn't in the https:// part
  24. return result
  25. def setup(app):
  26. rev = get_github_rev()
  27. # links to files or folders on the GitHub
  28. app.add_role('idf', github_link('tree', rev, '/', app.config))
  29. app.add_role('idf_file', github_link('blob', rev, '/', app.config))
  30. app.add_role('idf_raw', github_link('raw', rev, '/', app.config))
  31. app.add_role('component', github_link('tree', rev, '/components/', app.config))
  32. app.add_role('component_file', github_link('blob', rev, '/components/', app.config))
  33. app.add_role('component_raw', github_link('raw', rev, '/components/', app.config))
  34. app.add_role('example', github_link('tree', rev, '/examples/', app.config))
  35. app.add_role('example_file', github_link('blob', rev, '/examples/', app.config))
  36. app.add_role('example_raw', github_link('raw', rev, '/examples/', app.config))
  37. # link to the current documentation file in specific language version
  38. app.add_role('link_to_translation', link_to_translation(app.config))
  39. return {'parallel_read_safe': True, 'parallel_write_safe': True, 'version': '0.3'}
  40. def github_link(link_type, rev, root_path, app_config):
  41. def role(name, rawtext, text, lineno, inliner, options={}, content=[]):
  42. msgs = []
  43. def warning(msg):
  44. system_msg = inliner.reporter.warning(msg)
  45. system_msg.line = lineno
  46. msgs.append(system_msg)
  47. BASE_URL = 'https://github.com/espressif/esp-idf'
  48. # search for a named link (:label<path>) with descriptive label vs a plain URL
  49. m = re.search(r'(.*)\s*<(.*)>', text)
  50. if m:
  51. link_text = m.group(1)
  52. link = m.group(2)
  53. else:
  54. link_text = text
  55. link = text
  56. rel_path = root_path + link
  57. abs_path = os.path.join(app_config.idf_path, rel_path.lstrip('/'))
  58. line_no = None
  59. url = url_join(BASE_URL, link_type, rev, rel_path)
  60. if '#L' in abs_path:
  61. # drop any URL line number from the file, line numbers take the form #Lnnn or #Lnnn-Lnnn for a range
  62. abs_path, line_no = abs_path.split('#L')
  63. line_no = re.search(r'^(\d+)(?:-L(\d+))?', line_no)
  64. if line_no is None:
  65. warning("Line number anchor in URL %s doesn't seem to be valid" % link)
  66. else:
  67. line_no = tuple(int(l) for l in line_no.groups() if l) # tuple of (nnn,) or (nnn, NNN) for ranges
  68. elif '#' in abs_path: # drop any other anchor from the line
  69. abs_path = abs_path.split('#')[0]
  70. warning("URL %s seems to contain an unusable anchor after the #, only line numbers are supported" % link)
  71. is_dir = (link_type == 'tree')
  72. if not os.path.exists(abs_path):
  73. warning("IDF path %s does not appear to exist (absolute path %s)" % (rel_path, abs_path))
  74. elif is_dir and not os.path.isdir(abs_path):
  75. # note these "wrong type" warnings are not strictly needed as GitHub will apply a redirect,
  76. # but the may become important in the future (plus make for cleaner links)
  77. warning("IDF path %s is not a directory but role :%s: is for linking to a directory, try :%s_file:" % (rel_path, name, name))
  78. elif not is_dir and os.path.isdir(abs_path):
  79. warning("IDF path %s is a directory but role :%s: is for linking to a file" % (rel_path, name))
  80. # check the line number is valid
  81. if line_no:
  82. if is_dir:
  83. warning("URL %s contains a line number anchor but role :%s: is for linking to a directory" % (rel_path, name, name))
  84. elif os.path.exists(abs_path) and not os.path.isdir(abs_path):
  85. with open(abs_path, "r") as f:
  86. lines = len(f.readlines())
  87. if any(True for l in line_no if l > lines):
  88. warning("URL %s specifies a range larger than file (file has %d lines)" % (rel_path, lines))
  89. if tuple(sorted(line_no)) != line_no: # second line number comes before first one!
  90. warning("URL %s specifies a backwards line number range" % rel_path)
  91. node = nodes.reference(rawtext, link_text, refuri=url, **options)
  92. return [node], msgs
  93. return role
  94. def link_to_translation(config):
  95. def role(name, rawtext, text, lineno, inliner, options={}, content=[]):
  96. (language, link_text) = text.split(':')
  97. docname = inliner.document.settings.env.docname
  98. doc_path = inliner.document.settings.env.doc2path(docname, None, None)
  99. return_path = '../' * doc_path.count('/') # path back to the root from 'docname'
  100. # then take off 3 more paths for language/release/targetname and build the new URL
  101. url = "{}.html".format(os.path.join(return_path, '../../..', language, config.release,
  102. config.idf_target, docname))
  103. node = nodes.reference(rawtext, link_text, refuri=url, **options)
  104. return [node], []
  105. return role