aboutsummaryrefslogtreecommitdiff
path: root/_exts/httpdomain/httpdomain.py
diff options
context:
space:
mode:
Diffstat (limited to '_exts/httpdomain/httpdomain.py')
-rw-r--r--_exts/httpdomain/httpdomain.py773
1 files changed, 773 insertions, 0 deletions
diff --git a/_exts/httpdomain/httpdomain.py b/_exts/httpdomain/httpdomain.py
new file mode 100644
index 0000000..b31142b
--- /dev/null
+++ b/_exts/httpdomain/httpdomain.py
@@ -0,0 +1,773 @@
1"""
2 sphinxcontrib.httpdomain
3 ~~~~~~~~~~~~~~~~~~~~~~~~
4
5 The HTTP domain for documenting RESTful HTTP APIs.
6
7 :copyright: Copyright 2011 by Hong Minhee
8 :license: BSD, see LICENSE for details.
9
10"""
11
12import re
13
14from docutils import nodes
15
16from pygments.lexer import RegexLexer, bygroups
17from pygments.lexers import get_lexer_by_name
18from pygments.token import Literal, Text, Operator, Keyword, Name, Number
19from pygments.util import ClassNotFound
20
21from sphinx import addnodes
22from sphinx.roles import XRefRole
23from sphinx.domains import Domain, ObjType, Index
24from sphinx.directives import ObjectDescription, directives
25from sphinx.util.nodes import make_refnode
26from sphinx.util.docfields import GroupedField, TypedField
27
28# The env.get_doctree() lookup results in a pickle.load() call which is
29# expensive enough to dominate the runtime entirely when the number of endpoints
30# and references is large enough. The doctrees are generated during the read-
31# phase and we can cache their lookup during the write-phase significantly
32# improving performance.
33# Currently sphinxcontrib-httpdomain does not declare to support parallel read
34# support (parallel_read_safe is the default False) so we can simply use a
35# module global to hold the cache.
36_doctree_cache = {}
37
38
39class DocRef(object):
40 """Represents a reference to an abstract specification."""
41
42 def __init__(self, base_url, anchor, section):
43 self.base_url = base_url
44 self.anchor = anchor
45 self.section = section
46
47 def __repr__(self):
48 """Returns the URL onto related specification section for the related
49 object."""
50 return '{0}#{1}{2}'.format(self.base_url, self.anchor, self.section)
51
52
53class RFC2616Ref(DocRef):
54 """Represents a reference to RFC2616.
55 In 2014, RFC2616 was replaced by multiple RFCs (7230-7237)."""
56
57 def __init__(self, section):
58 url = 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec{0:d}.html'
59 url = url.format(int(section))
60 super(RFC2616Ref, self).__init__(url, 'sec', section)
61
62
63class IETFRef(DocRef):
64 """Represents a reference to the specific IETF RFC."""
65
66 def __init__(self, rfc, section):
67 url = 'https://tools.ietf.org/html/rfc{0:d}'.format(rfc)
68 super(IETFRef, self).__init__(url, 'section-', section)
69
70
71class EventSourceRef(DocRef):
72
73 def __init__(self, section):
74 url = 'http://www.w3.org/TR/eventsource/'
75 super(EventSourceRef, self).__init__(url, section, '')
76
77
78class CORSRef(DocRef):
79 """Represents a reference to W3 Cross-Origin Resource Sharing recommendation."""
80
81 def __init__(self, name, type):
82 url = 'http://www.w3.org/TR/cors/'
83 super(CORSRef, self).__init__(url, name, '-' + type)
84
85
86#: Mapping from lowercase HTTP method name to :class:`DocRef` object which
87#: maintains the URL which points to the section of the RFC which defines that
88#: HTTP method.
89METHOD_REFS = {
90 'patch': IETFRef(5789, 2),
91 'options': IETFRef(7231, '4.3.7'),
92 'get': IETFRef(7231, '4.3.1'),
93 'head': IETFRef(7231, '4.3.2'),
94 'post': IETFRef(7231, '4.3.3'),
95 'put': IETFRef(7231, '4.3.4'),
96 'delete': IETFRef(7231, '4.3.5'),
97 'trace': IETFRef(7231, '4.3.8'),
98 'connect': IETFRef(7231, '4.3.6'),
99 'copy': IETFRef(2518, 8.8),
100 'any': ''
101}
102
103
104#: Mapping from HTTP header name to :class:`DocRef` object which
105#: maintains the URL which points to the related section of the RFC.
106HEADER_REFS = {
107 'Accept': IETFRef(7231, '5.3.2'),
108 'Accept-Charset': IETFRef(7231, '5.3.3'),
109 'Accept-Encoding': IETFRef(7231, '5.3.4'),
110 'Accept-Language': IETFRef(7231, '5.3.5'),
111 'Accept-Ranges': IETFRef(7233, 2.3),
112 'Age': IETFRef(7234, 5.1),
113 'Allow': IETFRef(7231, '7.4.1'),
114 'Authorization': IETFRef(7235, 4.2),
115 'Cache-Control': IETFRef(7234, 5.2),
116 'Connection': IETFRef(7230, 6.1),
117 'Content-Encoding': IETFRef(7231, '3.1.2.2'),
118 'Content-Language': IETFRef(7231, '3.1.3.2'),
119 'Content-Length': IETFRef(7230, '3.3.2'),
120 'Content-Location': IETFRef(7231, '3.1.4.2'),
121 'Content-MD5': RFC2616Ref(14.15), # removed
122 'Content-Range': IETFRef(7233, 4.2),
123 'Content-Type': IETFRef(7231, '3.1.1.5'),
124 'Cookie': IETFRef(2109, '4.3.4'), # also RFC6265 section 5.4
125 'Date': IETFRef(7231, '7.1.1.2'),
126 'Destination': IETFRef(2518, 9.3),
127 'ETag': IETFRef(7232, 2.3),
128 'Expect': IETFRef(7231, '5.1.1'),
129 'Expires': IETFRef(7234, 5.3),
130 'From': IETFRef(7231, '5.5.2'),
131 'Host': IETFRef(7230, 5.4),
132 'If-Match': IETFRef(7232, 3.1),
133 'If-Modified-Since': IETFRef(7232, 3.3),
134 'If-None-Match': IETFRef(7232, 3.2),
135 'If-Range': IETFRef(7233, 3.2),
136 'If-Unmodified-Since': IETFRef(7232, 3.4),
137 'Last-Event-ID': EventSourceRef('last-event-id'),
138 'Last-Modified': IETFRef(7232, 2.2),
139 'Link': IETFRef(5988, '5'),
140 'Location': IETFRef(7231, '7.1.2'),
141 'Max-Forwards': IETFRef(7231, '5.1.2'),
142 'Pragma': IETFRef(7234, 5.4),
143 'Proxy-Authenticate': IETFRef(7235, 4.3),
144 'Proxy-Authorization': IETFRef(7235, 4.4),
145 'Range': IETFRef(7233, 3.1),
146 'Referer': IETFRef(7231, '5.5.2'),
147 'Retry-After': IETFRef(7231, '7.1.3'),
148 'Server': IETFRef(7231, '7.4.2'),
149 'Set-Cookie': IETFRef(2109, '4.2.2'),
150 'TE': IETFRef(7230, 4.3),
151 'Trailer': IETFRef(7230, 4.4),
152 'Transfer-Encoding': IETFRef(7230, '3.3.1'),
153 'Upgrade': IETFRef(7230, 6.7),
154 'User-Agent': IETFRef(7231, '5.5.3'),
155 'Vary': IETFRef(7231, '7.1.4'),
156 'Via': IETFRef(7230, '5.7.1'),
157 'Warning': IETFRef(7234, 5.5),
158 'WWW-Authenticate': IETFRef(7235, 4.1),
159 'Access-Control-Allow-Origin': CORSRef('access-control-allow-origin',
160 'response-header'),
161 'Access-Control-Allow-Credentials': CORSRef('access-control-allow-credentials',
162 'response-header'),
163 'Access-Control-Expose-Headers': CORSRef('access-control-expose-headers',
164 'response-header'),
165 'Access-Control-Max-Age': CORSRef('access-control-max-age',
166 'response-header'),
167 'Access-Control-Allow-Methods': CORSRef('access-control-allow-methods',
168 'response-header'),
169 'Access-Control-Allow-Headers': CORSRef('access-control-allow-headers',
170 'response-header'),
171 'Origin': CORSRef('origin', 'request-header'),
172 'Access-Control-Request-Method': CORSRef('access-control-request-method',
173 'response-header'),
174 'Access-Control-Request-Headers': CORSRef('access-control-request-headers',
175 'response-header'),
176}
177
178
179HTTP_STATUS_CODES = {
180 100: 'Continue',
181 101: 'Switching Protocols',
182 102: 'Processing',
183 200: 'OK',
184 201: 'Created',
185 202: 'Accepted',
186 203: 'Non Authoritative Information',
187 204: 'No Content',
188 205: 'Reset Content',
189 206: 'Partial Content',
190 207: 'Multi Status',
191 226: 'IM Used', # see RFC 3229
192 300: 'Multiple Choices',
193 301: 'Moved Permanently',
194 302: 'Found',
195 303: 'See Other',
196 304: 'Not Modified',
197 305: 'Use Proxy',
198 307: 'Temporary Redirect',
199 308: 'Permanent Redirect',
200 400: 'Bad Request',
201 401: 'Unauthorized',
202 402: 'Payment Required', # unused
203 403: 'Forbidden',
204 404: 'Not Found',
205 405: 'Method Not Allowed',
206 406: 'Not Acceptable',
207 407: 'Proxy Authentication Required',
208 408: 'Request Timeout',
209 409: 'Conflict',
210 410: 'Gone',
211 411: 'Length Required',
212 412: 'Precondition Failed',
213 413: 'Request Entity Too Large',
214 414: 'Request URI Too Long',
215 415: 'Unsupported Media Type',
216 416: 'Requested Range Not Satisfiable',
217 417: 'Expectation Failed',
218 418: "I'm a teapot", # see RFC 2324
219 422: 'Unprocessable Entity',
220 423: 'Locked',
221 424: 'Failed Dependency',
222 425: 'Too Early', # RFC 8470
223 426: 'Upgrade Required',
224 429: 'Too Many Requests',
225 449: 'Retry With', # proprietary MS extension
226 451: 'Unavailable For Legal Reasons',
227 500: 'Internal Server Error',
228 501: 'Not Implemented',
229 502: 'Bad Gateway',
230 503: 'Service Unavailable',
231 504: 'Gateway Timeout',
232 505: 'HTTP Version Not Supported',
233 507: 'Insufficient Storage',
234 510: 'Not Extended'
235}
236
237WEBDAV_STATUS_CODES = [207, 422, 423, 424, 507]
238
239http_sig_param_re = re.compile(r'\((?:(?P<type>[^:)]+):)?(?P<name>[\w_]+)\)',
240 re.VERBOSE)
241
242
243def sort_by_method(entries):
244 def cmp(item):
245 order = ['HEAD', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH',
246 'OPTIONS', 'TRACE', 'CONNECT', 'COPY', 'ANY']
247 method = item[0].split(' ', 1)[0]
248 if method in order:
249 return order.index(method)
250 return 100
251 return sorted(entries, key=cmp)
252
253
254def http_resource_anchor(method, path):
255 path = re.sub(r'[{}]', '', re.sub(r'[<>:/]', '-', path))
256 return method.lower() + '-' + path
257
258
259class HTTPResource(ObjectDescription):
260
261 doc_field_types = [
262 TypedField('parameter', label='Parameters',
263 names=('param', 'parameter', 'arg', 'argument'),
264 typerolename='obj', typenames=('paramtype', 'type')),
265 TypedField('jsonparameter', label='JSON Parameters',
266 names=('jsonparameter', 'jsonparam', 'json'),
267 typerolename='obj', typenames=('jsonparamtype', 'jsontype')),
268 TypedField('requestjsonobject', label='Request JSON Object',
269 names=('reqjsonobj', 'reqjson', '<jsonobj', '<json'),
270 typerolename='obj', typenames=('reqjsonobj', '<jsonobj')),
271 TypedField('requestjsonarray', label='Request JSON Array of Objects',
272 names=('reqjsonarr', '<jsonarr'),
273 typerolename='obj',
274 typenames=('reqjsonarrtype', '<jsonarrtype')),
275 TypedField('responsejsonobject', label='Response JSON Object',
276 names=('resjsonobj', 'resjson', '>jsonobj', '>json'),
277 typerolename='obj', typenames=('resjsonobj', '>jsonobj')),
278 TypedField('responsejsonarray', label='Response JSON Array of Objects',
279 names=('resjsonarr', '>jsonarr'),
280 typerolename='obj',
281 typenames=('resjsonarrtype', '>jsonarrtype')),
282 TypedField('queryparameter', label='Query Parameters',
283 names=('queryparameter', 'queryparam', 'qparam', 'query'),
284 typerolename='obj',
285 typenames=('queryparamtype', 'querytype', 'qtype')),
286 GroupedField('formparameter', label='Form Parameters',
287 names=('formparameter', 'formparam', 'fparam', 'form')),
288 GroupedField('requestheader', label='Request Headers',
289 rolename='header',
290 names=('<header', 'reqheader', 'requestheader')),
291 GroupedField('responseheader', label='Response Headers',
292 rolename='header',
293 names=('>header', 'resheader', 'responseheader')),
294 GroupedField('statuscode', label='Status Codes',
295 rolename='statuscode',
296 names=('statuscode', 'status', 'code'))
297 ]
298
299 option_spec = {
300 'deprecated': directives.flag,
301 'noindex': directives.flag,
302 'synopsis': lambda x: x,
303 }
304
305 method = NotImplemented
306
307 def handle_signature(self, sig, signode):
308 method = self.method.upper() + ' '
309 signode += addnodes.desc_name(method, method)
310 offset = 0
311 path = None
312 for match in http_sig_param_re.finditer(sig):
313 path = sig[offset:match.start()]
314 signode += addnodes.desc_name(path, path)
315 params = addnodes.desc_parameterlist()
316 typ = match.group('type')
317 if typ:
318 typ += ': '
319 params += addnodes.desc_annotation(typ, typ)
320 name = match.group('name')
321 params += addnodes.desc_parameter(name, name)
322 signode += params
323 offset = match.end()
324 if offset < len(sig):
325 path = sig[offset:len(sig)]
326 signode += addnodes.desc_name(path, path)
327 assert path is not None, 'no matches for sig: %s' % sig
328 fullname = self.method.upper() + ' ' + path
329 signode['method'] = self.method
330 signode['path'] = sig
331 signode['fullname'] = fullname
332 return (fullname, self.method, sig)
333
334 def needs_arglist(self):
335 return False
336
337 def add_target_and_index(self, name_cls, sig, signode):
338 signode['ids'].append(http_resource_anchor(*name_cls[1:]))
339 if 'noindex' not in self.options:
340 self.env.domaindata['http'][self.method][sig] = (
341 self.env.docname,
342 self.options.get('synopsis', ''),
343 'deprecated' in self.options)
344
345 def get_index_text(self, modname, name):
346 return ''
347
348
349class HTTPOptions(HTTPResource):
350
351 method = 'options'
352
353
354class HTTPHead(HTTPResource):
355
356 method = 'head'
357
358
359class HTTPPatch(HTTPResource):
360
361 method = 'patch'
362
363
364class HTTPPost(HTTPResource):
365
366 method = 'post'
367
368
369class HTTPGet(HTTPResource):
370
371 method = 'get'
372
373
374class HTTPPut(HTTPResource):
375
376 method = 'put'
377
378
379class HTTPDelete(HTTPResource):
380
381 method = 'delete'
382
383
384class HTTPTrace(HTTPResource):
385
386 method = 'trace'
387
388
389class HTTPConnect(HTTPResource):
390
391 method = 'connect'
392
393
394class HTTPCopy(HTTPResource):
395
396 method = 'copy'
397
398
399class HTTPAny(HTTPResource):
400
401 method = 'any'
402
403
404class HTTPXRefRole(XRefRole):
405
406 def __init__(self, method, **kwargs):
407 XRefRole.__init__(self, **kwargs)
408 self.method = method
409
410 def process_link(self, env, refnode, has_explicit_title, title, target):
411 if not has_explicit_title:
412 title = self.method.upper() + ' ' + title
413 return title, target
414
415
416class HTTPXRefMethodRole(XRefRole):
417
418 def result_nodes(self, document, env, node, is_ref):
419 method = node[0][0].lower()
420 rawsource = node[0].rawsource
421 config = env.domains['http'].env.config
422 if method not in METHOD_REFS:
423 if not config['http_strict_mode']:
424 return [nodes.emphasis(method, method)], []
425 reporter = document.reporter
426 msg = reporter.error('%s is not valid HTTP method' % method,
427 line=node.line)
428 prb = nodes.problematic(method, method)
429 return [prb], [msg]
430 url = str(METHOD_REFS[method])
431 if not url:
432 return [nodes.emphasis(method, method)], []
433 node = nodes.reference(rawsource, method.upper(), refuri=url)
434 return [node], []
435
436
437class HTTPXRefStatusRole(XRefRole):
438
439 def result_nodes(self, document, env, node, is_ref):
440 def get_code_status(text):
441 if text.isdigit():
442 code = int(text)
443 return code, HTTP_STATUS_CODES.get(code)
444 else:
445 try:
446 code, status = re.split(r'\s', text.strip(), 1)
447 code = int(code)
448 except ValueError:
449 return None, None
450 known_status = HTTP_STATUS_CODES.get(code)
451 if known_status is None:
452 return code, None
453 elif known_status.lower() != status.lower():
454 return code, None
455 else:
456 return code, status
457
458 def report_unknown_code():
459 if not config['http_strict_mode']:
460 return [nodes.emphasis(text, text)], []
461 reporter = document.reporter
462 msg = reporter.error('%d is unknown HTTP status code' % code,
463 line=node.line)
464 prb = nodes.problematic(text, text)
465 return [prb], [msg]
466
467 def report_invalid_code():
468 if not config['http_strict_mode']:
469 return [nodes.emphasis(text, text)], []
470 reporter = document.reporter
471 msg = reporter.error(
472 'HTTP status code must be an integer (e.g. `200`) or '
473 'start with an integer (e.g. `200 OK`); %r is invalid' %
474 text,
475 line=node.line
476 )
477 prb = nodes.problematic(text, text)
478 return [prb], [msg]
479
480 text = node[0][0]
481 rawsource = node[0].rawsource
482 config = env.domains['http'].env.config
483
484 code, status = get_code_status(text)
485 if code is None:
486 return report_invalid_code()
487 elif status is None:
488 return report_unknown_code()
489 elif code == 226:
490 url = 'http://www.ietf.org/rfc/rfc3229.txt'
491 elif code == 418:
492 url = 'http://www.ietf.org/rfc/rfc2324.txt'
493 elif code == 429:
494 url = 'http://tools.ietf.org/html/rfc6585#section-4'
495 elif code == 449:
496 url = 'http://msdn.microsoft.com/en-us/library/dd891478(v=prot.10).aspx'
497 elif code == 451:
498 url = 'http://www.ietf.org/rfc/rfc7725.txt'
499 elif code in WEBDAV_STATUS_CODES:
500 url = 'http://tools.ietf.org/html/rfc4918#section-11.%d' % (WEBDAV_STATUS_CODES.index(code) + 1)
501 elif code in HTTP_STATUS_CODES:
502 url = 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html' \
503 '#sec10.' + ('%d.%d' % (code // 100, 1 + code % 100))
504 else:
505 url = ''
506 node = nodes.reference(rawsource, '%d %s' % (code, status), refuri=url)
507 return [node], []
508
509
510class HTTPXRefHeaderRole(XRefRole):
511
512 def result_nodes(self, document, env, node, is_ref):
513 header = node[0][0]
514 rawsource = node[0].rawsource
515 if header not in HEADER_REFS:
516 _header = '-'.join(map(lambda i: i.title(), header.split('-')))
517 if _header not in HEADER_REFS:
518 return [nodes.emphasis(header, header)], []
519 url = str(HEADER_REFS[header])
520 node = nodes.reference(rawsource, header, refuri=url)
521 return [node], []
522
523
524class HTTPIndex(Index):
525
526 name = 'routingtable'
527 localname = 'HTTP Routing Table'
528 shortname = 'routing table'
529
530 def __init__(self, *args, **kwargs):
531 super(HTTPIndex, self).__init__(*args, **kwargs)
532
533 self.ignore = [
534 [l for l in x.split('/') if l]
535 for x in self.domain.env.config['http_index_ignore_prefixes']]
536 self.ignore.sort(reverse=True)
537
538 # During HTML generation these values pick from class,
539 # not from instance so we have a little hack the system
540 cls = self.__class__
541 cls.shortname = self.domain.env.config['http_index_shortname']
542 cls.localname = self.domain.env.config['http_index_localname']
543
544 def grouping_prefix(self, path):
545 letters = [x for x in path.split('/') if x]
546 for prefix in self.ignore:
547 if letters[:len(prefix)] == prefix:
548 return '/' + '/'.join(letters[:len(prefix) + 1])
549 return '/%s' % (letters[0] if letters else '',)
550
551 def generate(self, docnames=None):
552 content = {}
553 items = ((method, path, info)
554 for method, routes in self.domain.routes.items()
555 for path, info in routes.items())
556 items = sorted(items, key=lambda item: item[1])
557 for method, path, info in items:
558 entries = content.setdefault(self.grouping_prefix(path), [])
559 entries.append([
560 method.upper() + ' ' + path, 0, info[0],
561 http_resource_anchor(method, path),
562 '', 'Deprecated' if info[2] else '', info[1]
563 ])
564 items = sorted(
565 (path, sort_by_method(entries))
566 for path, entries in content.items()
567 )
568 return (items, True)
569
570
571class HTTPDomain(Domain):
572 """HTTP domain."""
573
574 name = 'http'
575 label = 'HTTP'
576
577 object_types = {
578 'options': ObjType('options', 'options', 'obj'),
579 'head': ObjType('head', 'head', 'obj'),
580 'post': ObjType('post', 'post', 'obj'),
581 'get': ObjType('get', 'get', 'obj'),
582 'put': ObjType('put', 'put', 'obj'),
583 'patch': ObjType('patch', 'patch', 'obj'),
584 'delete': ObjType('delete', 'delete', 'obj'),
585 'trace': ObjType('trace', 'trace', 'obj'),
586 'connect': ObjType('connect', 'connect', 'obj'),
587 'copy': ObjType('copy', 'copy', 'obj'),
588 'any': ObjType('any', 'any', 'obj')
589 }
590
591 directives = {
592 'options': HTTPOptions,
593 'head': HTTPHead,
594 'post': HTTPPost,
595 'get': HTTPGet,
596 'put': HTTPPut,
597 'patch': HTTPPatch,
598 'delete': HTTPDelete,
599 'trace': HTTPTrace,
600 'connect': HTTPConnect,
601 'copy': HTTPCopy,
602 'any': HTTPAny
603 }
604
605 roles = {
606 'options': HTTPXRefRole('options'),
607 'head': HTTPXRefRole('head'),
608 'post': HTTPXRefRole('post'),
609 'get': HTTPXRefRole('get'),
610 'put': HTTPXRefRole('put'),
611 'patch': HTTPXRefRole('patch'),
612 'delete': HTTPXRefRole('delete'),
613 'trace': HTTPXRefRole('trace'),
614 'connect': HTTPXRefRole('connect'),
615 'copy': HTTPXRefRole('copy'),
616 'any': HTTPXRefRole('any'),
617 'statuscode': HTTPXRefStatusRole(),
618 'method': HTTPXRefMethodRole(),
619 'header': HTTPXRefHeaderRole()
620 }
621
622 initial_data = {
623 'options': {}, # path: (docname, synopsis)
624 'head': {},
625 'post': {},
626 'get': {},
627 'put': {},
628 'patch': {},
629 'delete': {},
630 'trace': {},
631 'connect': {},
632 'copy': {},
633 'any': {}
634 }
635
636 indices = []
637
638 @property
639 def routes(self):
640 return dict((key, self.data[key]) for key in self.object_types)
641
642 def clear_doc(self, docname):
643 for typ, routes in self.routes.items():
644 for path, info in list(routes.items()):
645 if info[0] == docname:
646 del routes[path]
647
648 def resolve_xref(self, env, fromdocname, builder, typ, target,
649 node, contnode):
650 try:
651 info = self.data[str(typ)][target]
652 except KeyError:
653 text = contnode.rawsource
654 role = self.roles.get(typ)
655 if role is None:
656 return None
657
658 if fromdocname not in _doctree_cache:
659 _doctree_cache[fromdocname] = env.get_doctree(fromdocname)
660 doctree = _doctree_cache[fromdocname]
661
662 resnode = role.result_nodes(doctree, env, node, None)[0][0]
663 if isinstance(resnode, addnodes.pending_xref):
664 text = node[0][0]
665 reporter = doctree.reporter
666 reporter.warning('Cannot resolve reference to %r' % text,
667 line=node.line)
668 return None
669 return resnode
670 else:
671 anchor = http_resource_anchor(typ, target)
672 title = typ.upper() + ' ' + target
673 return make_refnode(builder, fromdocname, info[0], anchor,
674 contnode, title)
675
676 def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode):
677 """Resolve the pending_xref *node* with the given *target*.
678
679 The reference comes from an "any" or similar role, which means that Sphinx
680 don't know the type.
681
682 For now sphinxcontrib-httpdomain doesn't resolve any xref nodes.
683
684 :return:
685 list of tuples ``('domain:role', newnode)``, where ``'domain:role'``
686 is the name of a role that could have created the same reference,
687 """
688 return []
689
690 def get_objects(self):
691 for method, routes in self.routes.items():
692 for path, info in routes.items():
693 anchor = http_resource_anchor(method, path)
694 yield (path, path, method, info[0], anchor, 1)
695
696
697class HTTPLexer(RegexLexer):
698 """Lexer for HTTP sessions."""
699
700 name = 'HTTP'
701 aliases = ['http']
702
703 flags = re.DOTALL
704
705 def header_callback(self, match):
706 if match.group(1).lower() == 'content-type':
707 content_type = match.group(5).strip()
708 if ';' in content_type:
709 content_type = content_type[:content_type.find(';')].strip()
710 self.content_type = content_type
711 yield match.start(1), Name.Attribute, match.group(1)
712 yield match.start(2), Text, match.group(2)
713 yield match.start(3), Operator, match.group(3)
714 yield match.start(4), Text, match.group(4)
715 yield match.start(5), Literal, match.group(5)
716 yield match.start(6), Text, match.group(6)
717
718 def continuous_header_callback(self, match):
719 yield match.start(1), Text, match.group(1)
720 yield match.start(2), Literal, match.group(2)
721 yield match.start(3), Text, match.group(3)
722
723 def content_callback(self, match):
724 content_type = getattr(self, 'content_type', None)
725 content = match.group()
726 offset = match.start()
727 if content_type:
728 from pygments.lexers import get_lexer_for_mimetype
729 try:
730 lexer = get_lexer_for_mimetype(content_type)
731 except ClassNotFound:
732 pass
733 else:
734 for idx, token, value in lexer.get_tokens_unprocessed(content):
735 yield offset + idx, token, value
736 return
737 yield offset, Text, content
738
739 tokens = {
740 'root': [
741 (r'(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|COPY)( +)([^ ]+)( +)'
742 r'(HTTPS?)(/)(1\.[01])(\r?\n|$)',
743 bygroups(Name.Function, Text, Name.Namespace, Text,
744 Keyword.Reserved, Operator, Number, Text),
745 'headers'),
746 (r'(HTTPS?)(/)(1\.[01])( +)(\d{3})( +)([^\r\n]+)(\r?\n|$)',
747 bygroups(Keyword.Reserved, Operator, Number, Text, Number,
748 Text, Name.Exception, Text),
749 'headers'),
750 ],
751 'headers': [
752 (r'([^\s:]+)( *)(:)( *)([^\r\n]+)(\r?\n|$)', header_callback),
753 (r'([\t ]+)([^\r\n]+)(\r?\n|$)', continuous_header_callback),
754 (r'\r?\n', Text, 'content')
755 ],
756 'content': [
757 (r'.+', content_callback)
758 ]
759 }
760
761
762def setup(app):
763 app.add_domain(HTTPDomain)
764
765 try:
766 get_lexer_by_name('http')
767 except ClassNotFound:
768 app.add_lexer('http', HTTPLexer())
769 app.add_config_value('http_index_ignore_prefixes', [], None)
770 app.add_config_value('http_index_shortname', 'routing table', True)
771 app.add_config_value('http_index_localname', 'HTTP Routing Table', True)
772 app.add_config_value('http_strict_mode', True, None)
773 app.add_config_value('http_headers_ignore_prefixes', ['X-'], None)