1from docutils import nodes
2import itertools
3import string
4
5##################################################
6### Utility functions.
7##################################################
8
9def print_error(err_string):
10    """
11    Print the err_string in red font.
12    """
13
14    FAIL = '\033[91m'
15    ENDC = '\033[0m'
16    print(FAIL + err_string + ENDC)
17
18
19def get_project(inliner):
20    """
21    Returns the project name associated with the file being processed.
22    This is used when generating _implicit_ links.
23    """
24
25    return inliner.document.settings.env.app.config.project
26
27
28def convert_special_chars_to_ascii(func_name):
29    """
30    Convert non-alphanumeric characters to their ascii representation.
31
32    This is how Haddock generates links for operators.
33
34    '!?' => '-33--63-'
35    """
36
37    if func_name == None:
38        return None
39
40    escaped_func_name = [ c if c not in string.punctuation else '-' + str(ord(c)) + '-'
41                          for c in func_name ]
42
43    return ''.join(escaped_func_name)
44
45
46def parse_haddock_ref_text(text):
47    """
48    Parses text of the form pkg-name/Module.Path#ident into the tuple
49    (package, module, ident).
50
51    The module and function name are optional, if they are omitted then 'None'
52    will be returned in the corresponding tuple element. If this is an implicit
53    reference (/Module.Path#ident) then 'package' will be 'None'.
54
55    Example inputs:
56      Explicit package references:
57      'pkg' => ('package_name', None, None)
58      'pkg/Module.Name' => ('pkg', 'Module.Name', None)
59      'pkg/Module.Name#ident' => ('pkg', 'Module.Name', 'ident')
60
61      Implicit package references:
62      '/Module.Name' => (None, 'Module.Name', None)
63      '/Module.Name#ident' => (None, 'Module.Name', 'ident')
64    """
65
66    # If there's no '/' then this is a reference to a package.
67    # A module or identifier reference is invalid here.
68    if '/' not in text:
69        if '#' in text:
70            print_error('Invalid haddock reference: ' + text)
71            raise Exception('Invalid haddock reference, see error above.')
72            return (None, None, None)
73        else:
74            return (text, None, None)
75
76    # Leading '/' means local package reference.
77    # Calling code responsible for determining what "local" means.
78    if 0 == text.find('/'):
79        text = text[1:]
80        if '#' in text:
81            module,ident = text.split('#')
82            return (None, module, ident)
83        else:
84            return (None, text, None)
85
86    # Otherwise, explicit reference.
87    pkg_name,rest = text.split('/')
88
89    ident = None
90    module = None
91    if '#' in rest:
92        module,ident = rest.split('#')
93    else:
94        module = rest
95
96    return (pkg_name, module, ident)
97
98
99##################################################
100### Link generation functions.
101##################################################
102
103
104def pkg_root_ref(haddock_host, haddock_root, pkg):
105    """
106    Returns the URI for the root of pkg's Haddocks.
107
108    Note: Hackage uses a different URI scheme than stackage and local.
109
110    URI enclosed in {} corresponds to 'haddock_root'.
111
112    Hackage: {hackage.haskell.org/package/}<pkg_name>
113    Stackage: {www.stackage.org/haddock/<resolver>/}<pkg_name>/index.html
114    Local: {file:///path/to/html/}<pkg_name>/index.html
115    """
116
117    if haddock_host == 'hackage':
118        return haddock_root + pkg
119
120    if haddock_host == 'stackage':
121        return haddock_root + pkg + '/index.html'
122
123    if haddock_host == 'local':
124        return haddock_root + pkg + '/index.html'
125
126
127def module_ref(haddock_host, haddock_root, pkg, module, func_name):
128    """
129    Returns the URI referring to pkg/module#func_name.
130
131    Note: Hackage uses a different URI scheme than stackage and local.
132
133    URI enclosed in {} corresponds to 'haddock_root'.
134
135    Hackage: {hackage.haskell.org/package/}<pkg_name>/docs/<module>.html#v:<func_name>
136    Stackage: {www.stackage.org/haddock/<resolver>/}<pkg_name>/<module>.html#t:<func_name>
137    Local: {file:///path/to/html/}<pkg_name>/<module>.html#t:<func_name>
138    """
139
140    if module != None:
141        module = module.replace('.', '-')
142
143    if haddock_host == 'hackage':
144        ref = haddock_root + pkg + '/docs/' + module + '.html'
145
146    if haddock_host == 'stackage':
147        ref = haddock_root + pkg + '/' + module + '.html'
148
149    if haddock_host == 'local':
150        ref = haddock_root + pkg + '/' + module + '.html'
151
152    # If a function name was provided, link to it.
153    if func_name != None:
154        # Select the correct anchor, types use #t, functions use #v.
155        # TODO(m-renuad): Determine if there's cases where this is incorrect.
156        if func_name[0].isupper():
157            anchor_type = '#t:'
158        else:
159            anchor_type = '#v:'
160        ref = ref + anchor_type + func_name
161
162    return ref
163
164
165def haddock_ref(haddock_host, haddock_root, pkg, module, func_name):
166    """
167    Return a reference link to Haddocks for pkg/module#func_name.
168    """
169
170    if module == None and func_name == None:
171        return pkg_root_ref(haddock_host, haddock_root, pkg)
172    else:
173        func_name = convert_special_chars_to_ascii(func_name)
174        return module_ref(haddock_host, haddock_root, pkg, module, func_name)
175
176
177
178#############################################
179# -- Custom roles for linking to Hackage docs
180#############################################
181
182# Support building docs with link to hackage, stackage, or locally build haddocks.
183# Valid options:
184#   hackage - link to hackage.haskell.org
185#   stackage - link to www.stackage.org (must also pass STACKAGE_RESOLVER)
186#   local - any path to local docs (must also set HADDOCK_DIR)
187#
188# Note: Defaults to hackage if none specified.
189#
190# Note: We need to do some custom URL rewriting for stackage because it uses a different
191# format from what the haddock tool builds
192#
193# TODO(m-renaud): Improve this and publish as sphinx extension.
194
195### URI scheme examples for Hackage, Stackage, and local docs.
196## Packages
197# Hackage: hackage.haskell.org/package/containers
198# Stackage: www.stackage.org/haddock/lts-10.0/containers/index.html
199# Local: file:///local/path/html/containers/index.html
200
201## Module (and function) references
202# Hackage: hackage.haskell.org/package/containers/docs/Data.Set.html#v:empty
203# Stackage: www.stackage.org/haddock/lts-10.0/containers/Data.Set.html#t:empty
204# Local: file:///path/to/html/containers/Data.Set.html#t:empty
205
206class HaddockAutolinker:
207
208    def __init__(self, haddock_host, haddock_root):
209        self.haddock_host = haddock_host
210        self.haddock_root = haddock_root
211
212
213    def haddock_role(self, display_name_only=False):
214        def haddock_role_impl(name, rawtext, text, lineno, inliner, options={}, content=[]):
215            """
216            Role handler for :haddock:.
217            """
218
219            (pkg, module, ident) = parse_haddock_ref_text(text)
220
221            # If the pkg isn't set then this is a local reference to a module
222            # function in the current package.
223            if pkg == None:
224                pkg = get_project(inliner)
225            ref = haddock_ref(self.haddock_host, self.haddock_root, pkg, module, ident)
226
227            if ref == None:
228                print_error('ERROR: invalid argument to :' + name + ':')
229                print_error('  Markup: ' + str(rawtext))
230                print_error('  Line: ' + str(lineno))
231                raise Exception('Invalid Haddock link, see ERROR above.')
232
233            if module == None:
234                link_text = pkg
235            else:
236                if ident == None:
237                    link_text = module
238                else:
239                    if display_name_only:
240                        link_text = ident
241                    else:
242                        link_text = module + '#' + ident
243
244            node = nodes.reference(rawtext, link_text, refuri=ref, **options)
245            return [node], []
246        return haddock_role_impl
247