link_roles.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  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. from collections import namedtuple
  9. from sphinx.transforms.post_transforms import SphinxPostTransform
  10. from get_github_rev import get_github_rev
  11. # Creates a dict of all submodules with the format {submodule_path : (url relative to git root), commit)}
  12. def get_submodules():
  13. git_root = subprocess.check_output(['git', 'rev-parse', '--show-toplevel']).strip().decode('utf-8')
  14. gitmodules_file = os.path.join(git_root, '.gitmodules')
  15. submodules = subprocess.check_output(['git', 'submodule', 'status']).strip().decode('utf-8').split('\n')
  16. submodule_dict = {}
  17. Submodule = namedtuple('Submodule', 'url rev')
  18. for sub in submodules:
  19. sub_info = sub.lstrip().split(' ')
  20. # Get short hash, 7 digits
  21. rev = sub_info[0].lstrip('-')[0:7]
  22. path = sub_info[1].lstrip('./')
  23. config_key_arg = "submodule.{}.url".format(path)
  24. rel_url = subprocess.check_output(['git', 'config', '--file', gitmodules_file, '--get', config_key_arg]).decode('utf-8').lstrip('./').rstrip('\n')
  25. submodule_dict[path] = Submodule(rel_url, rev)
  26. return submodule_dict
  27. def url_join(*url_parts):
  28. """ Make a URL out of multiple components, assume first part is the https:// part and
  29. anything else is a path component """
  30. result = "/".join(url_parts)
  31. result = re.sub(r"([^:])//+", r"\1/", result) # remove any // that isn't in the https:// part
  32. return result
  33. def github_link(link_type, idf_rev, submods, root_path, app_config):
  34. def role(name, rawtext, text, lineno, inliner, options={}, content=[]):
  35. msgs = []
  36. BASE_URL = 'https://github.com/'
  37. IDF_REPO = "espressif/esp-idf"
  38. def warning(msg):
  39. system_msg = inliner.reporter.warning(msg)
  40. system_msg.line = lineno
  41. msgs.append(system_msg)
  42. # Redirects to submodule repo if path is a submodule, else default to IDF repo
  43. def redirect_submodule(path, submods, rev):
  44. for key, value in submods.items():
  45. # Add path separator to end of submodule path to ensure we are matching a directory
  46. if path.lstrip('/').startswith(os.path.join(key, '')):
  47. return value.url.replace('.git', ''), value.rev, re.sub('^/{}/'.format(key), '', path)
  48. return IDF_REPO, rev, path
  49. # search for a named link (:label<path>) with descriptive label vs a plain URL
  50. m = re.search(r'(.*)\s*<(.*)>', text)
  51. if m:
  52. link_text = m.group(1)
  53. link = m.group(2)
  54. else:
  55. link_text = text
  56. link = text
  57. rel_path = root_path + link
  58. abs_path = os.path.join(app_config.idf_path, rel_path.lstrip('/'))
  59. repo, repo_rev, rel_path = redirect_submodule(rel_path, submods, idf_rev)
  60. line_no = None
  61. url = url_join(BASE_URL, repo, link_type, repo_rev, rel_path)
  62. if '#L' in abs_path:
  63. # drop any URL line number from the file, line numbers take the form #Lnnn or #Lnnn-Lnnn for a range
  64. abs_path, line_no = abs_path.split('#L')
  65. line_no = re.search(r'^(\d+)(?:-L(\d+))?', line_no)
  66. if line_no is None:
  67. warning("Line number anchor in URL %s doesn't seem to be valid" % link)
  68. else:
  69. line_no = tuple(int(ln_group) for ln_group in line_no.groups() if ln_group) # tuple of (nnn,) or (nnn, NNN) for ranges
  70. elif '#' in abs_path: # drop any other anchor from the line
  71. abs_path = abs_path.split('#')[0]
  72. warning("URL %s seems to contain an unusable anchor after the #, only line numbers are supported" % link)
  73. is_dir = (link_type == 'tree')
  74. if not os.path.exists(abs_path):
  75. warning("IDF path %s does not appear to exist (absolute path %s)" % (rel_path, abs_path))
  76. elif is_dir and not os.path.isdir(abs_path):
  77. # note these "wrong type" warnings are not strictly needed as GitHub will apply a redirect,
  78. # but the may become important in the future (plus make for cleaner links)
  79. warning("IDF path %s is not a directory but role :%s: is for linking to a directory, try :%s_file:" % (rel_path, name, name))
  80. elif not is_dir and os.path.isdir(abs_path):
  81. warning("IDF path %s is a directory but role :%s: is for linking to a file" % (rel_path, name))
  82. # check the line number is valid
  83. if line_no:
  84. if is_dir:
  85. warning("URL %s contains a line number anchor but role :%s: is for linking to a directory" % (rel_path, name, name))
  86. elif os.path.exists(abs_path) and not os.path.isdir(abs_path):
  87. with open(abs_path, "r") as f:
  88. lines = len(f.readlines())
  89. if any(True for ln in line_no if ln > lines):
  90. warning("URL %s specifies a range larger than file (file has %d lines)" % (rel_path, lines))
  91. if tuple(sorted(line_no)) != line_no: # second line number comes before first one!
  92. warning("URL %s specifies a backwards line number range" % rel_path)
  93. node = nodes.reference(rawtext, link_text, refuri=url, **options)
  94. return [node], msgs
  95. return role
  96. class translation_link(nodes.Element):
  97. """Node for "link_to_translation" role."""
  98. # Linking to translation is done at the "writing" stage to avoid issues with the info being cached between builders
  99. def link_to_translation(name, rawtext, text, lineno, inliner, options={}, content=[]):
  100. node = translation_link()
  101. node['expr'] = (rawtext, text, options)
  102. return [node], []
  103. class TranslationLinkNodeTransform(SphinxPostTransform):
  104. # Transform needs to happen early to ensure the new reference node is also transformed
  105. default_priority = 0
  106. def run(self, **kwargs):
  107. # Only output relative links if building HTML
  108. for node in self.document.traverse(translation_link):
  109. if 'html' in self.app.builder.name:
  110. rawtext, text, options = node['expr']
  111. (language, link_text) = text.split(':')
  112. env = self.document.settings.env
  113. docname = env.docname
  114. doc_path = env.doc2path(docname, None, None)
  115. return_path = '../' * doc_path.count('/') # path back to the root from 'docname'
  116. # then take off 3 more paths for language/release/targetname and build the new URL
  117. url = "{}.html".format(os.path.join(return_path, '../../..', language, env.config.release,
  118. env.config.idf_target, docname))
  119. node.replace_self(nodes.reference(rawtext, link_text, refuri=url, **options))
  120. else:
  121. node.replace_self([])
  122. def setup(app):
  123. rev = get_github_rev()
  124. submods = get_submodules()
  125. # links to files or folders on the GitHub
  126. app.add_role('idf', github_link('tree', rev, submods, '/', app.config))
  127. app.add_role('idf_file', github_link('blob', rev, submods, '/', app.config))
  128. app.add_role('idf_raw', github_link('raw', rev, submods, '/', app.config))
  129. app.add_role('component', github_link('tree', rev, submods, '/components/', app.config))
  130. app.add_role('component_file', github_link('blob', rev, submods, '/components/', app.config))
  131. app.add_role('component_raw', github_link('raw', rev, submods, '/components/', app.config))
  132. app.add_role('example', github_link('tree', rev, submods, '/examples/', app.config))
  133. app.add_role('example_file', github_link('blob', rev, submods, '/examples/', app.config))
  134. app.add_role('example_raw', github_link('raw', rev, submods, '/examples/', app.config))
  135. # link to the current documentation file in specific language version
  136. app.add_role('link_to_translation', link_to_translation)
  137. app.add_node(translation_link)
  138. app.add_post_transform(TranslationLinkNodeTransform)
  139. return {'parallel_read_safe': True, 'parallel_write_safe': True, 'version': '0.5'}