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