link_roles.py 8.0 KB

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