├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── setup.py └── sphinx_http_domain ├── __init__.py ├── directives.py ├── docfields.py ├── nodes.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | build/ 4 | dist/ 5 | 6 | MANIFEST 7 | .svn/entries 8 | sphinx_http_domain/.svn/entries -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Software License Agreement (BSD License) 2 | 3 | Copyright (c) 2011, David Zentgraf. 4 | All rights reserved. 5 | 6 | Redistribution and use of this software in source and binary forms, with or without modification, are 7 | permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above 10 | copyright notice, this list of conditions and the 11 | following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the 15 | following disclaimer in the documentation and/or other 16 | materials provided with the distribution. 17 | 18 | * Neither the name of David Zentgraf nor the names of its 19 | contributors may be used to endorse or promote products 20 | derived from this software without specific prior 21 | written permission of David Zentgraf. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 24 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 25 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 28 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 29 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 30 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.rst 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | This package is no longer being maintained 2 | ========================================== 3 | 4 | Please use official Sphinx extensions (http://packages.python.org/sphinxcontrib-httpdomain/) 5 | -------------------------------------------------------------------------------------------- 6 | 7 | 8 | *______________________________________________________________* 9 | 10 | Sphinx HTTP Domain 11 | ================== 12 | 13 | Description 14 | ----------- 15 | 16 | Sphinx plugin to add an HTTP domain, allowing the documentation of 17 | RESTful HTTP methods. 18 | 19 | HTTP methods 20 | ------------ 21 | 22 | You can document simple methods, wrap any arguments in the path 23 | with curly-braces:: 24 | 25 | .. http:method:: GET /api/foo/bar/{id}/{slug} 26 | 27 | :arg id: An id 28 | :arg slug: A slug 29 | 30 | Retrieve list of foobars matching given id. 31 | 32 | Query string parameters are also supported, both mandatory and 33 | optional:: 34 | 35 | .. http:method:: GET /api/foo/bar/?id&slug 36 | 37 | :param id: An id 38 | :optparam slug: A slug 39 | 40 | Search for a list of foobars matching given id. 41 | 42 | As well, you can provide types for parameters and arguments:: 43 | 44 | .. http:method:: GET /api/foo/bar/{id}/?slug 45 | 46 | :arg integer id: An id 47 | :optparam string slug: A slug 48 | 49 | Search for a list of foobars matching given id. 50 | 51 | Fragments are also supported:: 52 | 53 | .. http:method:: GET /#!/username 54 | 55 | :fragment username: A username 56 | 57 | Renders a user's profile page. 58 | 59 | Plus, you can document the responses with their response codes:: 60 | 61 | .. http:method:: POST /api/foo/bar/ 62 | 63 | :param string slug: A slug 64 | :response 201: A foobar was created successfully. 65 | :response 400: 66 | 67 | Create a foobar. 68 | 69 | To refer to an HTTP method, use ``:http:method:``:: 70 | 71 | .. http:method:: GET /api/ 72 | :label-name: get-root 73 | :title: API root 74 | 75 | The :http:method:`get-root` contains all of the API. 76 | 77 | 78 | HTTP responses 79 | -------------- 80 | 81 | Documenting responses is also simple:: 82 | 83 | .. http:response:: Foobar object 84 | 85 | A foobar object looks like this:: 86 | 87 | .. source-code:: js 88 | { 89 | 'slug': SLUG 90 | } 91 | 92 | :data string SLUG: A slug 93 | :format: JSON 94 | 95 | To refer to an HTTP response, use ``:http:response:``:: 96 | 97 | .. http:response:: Foobar object 98 | 99 | A :http:response:`foobar-object` is returned when you foo the bar. 100 | 101 | 102 | Installation 103 | ------------ 104 | 105 | Requires Sphinx >= 1.0.6 (http://sphinx.pocoo.org). 106 | 107 | Run ``pip install sphinx-http-domain``. 108 | 109 | Then, add ``sphinx_http_domain`` to your conf.py:: 110 | 111 | extensions = ['sphinx_http_domain'] 112 | 113 | 114 | Development 115 | ----------- 116 | 117 | - Version: 0.2 118 | - Homepage: https://github.com/deceze/Sphinx-HTTP-domain 119 | 120 | For contributions, please fork this project on GitHub! 121 | 122 | 123 | Author 124 | `````` 125 | 126 | David Zentgraf (https://github.com/deceze) 127 | 128 | 129 | Contributors 130 | ```````````` 131 | 132 | - Simon Law (https://github.com/sfllaw), who really wrote virtually all of this 133 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | import os 3 | 4 | from distutils.core import setup 5 | 6 | 7 | with open('README.rst', 'r') as f: 8 | long_description = f.read() 9 | 10 | setup( 11 | name='sphinx-http-domain', 12 | version='0.2', 13 | description='Sphinx domain to mark up RESTful web services in ReST', 14 | long_description=long_description, 15 | url='https://github.com/deceze/Sphinx-HTTP-domain/', 16 | author='David Zentgraf', 17 | author_email='deceze@gmail.com', 18 | packages=['sphinx_http_domain'], 19 | requires=['Sphinx (>=1.0.7)'], 20 | zip_safe=True, 21 | classifiers=['Development Status :: 2 - Pre-Alpha', 22 | 'Environment :: Web Environment', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: BSD License', 25 | 'Programming Language :: Python', 26 | 'Topic :: Software Development :: Documentation'], 27 | ) 28 | -------------------------------------------------------------------------------- /sphinx_http_domain/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | sphinx.domains.http 4 | ~~~~~~~~~~~~~~~~~~~ 5 | 6 | The HTTP domain. 7 | 8 | :copyright: Copyright 2011, David Zentgraf. 9 | :license: BSD, see LICENSE for details 10 | """ 11 | 12 | from itertools import izip 13 | 14 | from docutils.nodes import literal, Text 15 | 16 | from sphinx.locale import l_ 17 | from sphinx.domains import Domain, ObjType 18 | from sphinx.roles import XRefRole 19 | from sphinx.util.nodes import make_refnode 20 | 21 | from sphinx_http_domain.directives import HTTPMethod, HTTPResponse 22 | from sphinx_http_domain.nodes import (desc_http_method, desc_http_url, 23 | desc_http_path, desc_http_patharg, 24 | desc_http_query, desc_http_queryparam, 25 | desc_http_fragment, desc_http_response) 26 | 27 | 28 | class HTTPDomain(Domain): 29 | """HTTP language domain.""" 30 | name = 'http' 31 | label = 'HTTP' 32 | object_types = { 33 | 'method': ObjType(l_('method'), 'method'), 34 | 'response': ObjType(l_('response'), 'response'), 35 | } 36 | directives = { 37 | 'method': HTTPMethod, 38 | 'response': HTTPResponse, 39 | } 40 | roles = { 41 | 'method': XRefRole(), 42 | 'response': XRefRole(), 43 | } 44 | initial_data = { 45 | 'method': {}, # name -> docname, sig, title, method 46 | 'response': {}, # name -> docname, sig, title 47 | } 48 | 49 | def clear_doc(self, docname): 50 | """Remove traces of a document from self.data.""" 51 | for typ in self.initial_data: 52 | for name, entry in self.data[typ].items(): 53 | if entry[0] == docname: 54 | del self.data[typ][name] 55 | 56 | def find_xref(self, env, typ, target): 57 | """Returns a self.data entry for *target*, according to *typ*.""" 58 | try: 59 | return self.data[typ][target] 60 | except KeyError: 61 | return None 62 | 63 | def resolve_xref(self, env, fromdocname, builder, 64 | typ, target, node, contnode): 65 | """ 66 | Resolve the ``pending_xref`` *node* with the given *typ* and *target*. 67 | 68 | Returns a new reference node, to replace the xref node. 69 | 70 | If no resolution can be found, returns None. 71 | """ 72 | match = self.find_xref(env, typ, target) 73 | if match: 74 | docname = match[0] 75 | sig = match[1] 76 | title = match[2] 77 | # Coerce contnode into the right nodetype 78 | nodetype = type(contnode) 79 | if issubclass(nodetype, literal): 80 | nodetype = self.directives[typ].nodetype 81 | # Override contnode with title, unless it has been manually 82 | # overridden in the text. 83 | if contnode.astext() == target: 84 | contnode = nodetype(title, title) 85 | else: 86 | child = contnode.children[0] 87 | contnode = nodetype(child, child) 88 | # Return the new reference node 89 | return make_refnode(builder, fromdocname, docname, 90 | typ + '-' + target, contnode, sig) 91 | 92 | def get_objects(self): 93 | """ 94 | Return an iterable of "object descriptions", which are tuples with 95 | five items: 96 | 97 | * `name` -- fully qualified name 98 | * `dispname` -- name to display when searching/linking 99 | * `type` -- object type, a key in ``self.object_types`` 100 | * `docname` -- the document where it is to be found 101 | * `anchor` -- the anchor name for the object 102 | * `priority` -- how "important" the object is (determines placement 103 | in search results) 104 | 105 | - 1: default priority (placed before full-text matches) 106 | - 0: object is important (placed before default-priority objects) 107 | - 2: object is unimportant (placed after full-text matches) 108 | - -1: object should not show up in search at all 109 | """ 110 | # Method descriptions 111 | for typ in self.initial_data: 112 | for name, entry in self.data[typ].iteritems(): 113 | docname = entry[0] 114 | yield(name, name, typ, docname, typ + '-' + name, 0) 115 | 116 | 117 | def setup(app): 118 | app.add_domain(HTTPDomain) 119 | desc_http_method.contribute_to_app(app) 120 | desc_http_url.contribute_to_app(app) 121 | desc_http_path.contribute_to_app(app) 122 | desc_http_patharg.contribute_to_app(app) 123 | desc_http_query.contribute_to_app(app) 124 | desc_http_queryparam.contribute_to_app(app) 125 | desc_http_fragment.contribute_to_app(app) 126 | desc_http_response.contribute_to_app(app) 127 | -------------------------------------------------------------------------------- /sphinx_http_domain/directives.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | sphinx.domains.http 4 | ~~~~~~~~~~~~~~~~~~~ 5 | 6 | Directives for the HTTP domain. 7 | """ 8 | 9 | import re 10 | from urlparse import urlsplit 11 | 12 | from docutils.nodes import literal, strong, Text 13 | from docutils.parsers.rst import directives 14 | 15 | from sphinx.locale import l_, _ 16 | from sphinx.directives import ObjectDescription 17 | from sphinx.util.docfields import TypedField 18 | 19 | from sphinx_http_domain.docfields import NoArgGroupedField, ResponseField 20 | from sphinx_http_domain.nodes import (desc_http_method, desc_http_url, 21 | desc_http_path, desc_http_patharg, 22 | desc_http_query, desc_http_queryparam, 23 | desc_http_fragment, desc_http_response) 24 | from sphinx_http_domain.utils import slugify, slugify_url 25 | 26 | try: 27 | from urlparse import parse_qsl 28 | except ImportError: 29 | from cgi import parse_qsl 30 | 31 | class HTTPDescription(ObjectDescription): 32 | def get_anchor(self, name, sig): 33 | """ 34 | Returns anchor for cross-reference IDs. 35 | 36 | *name* is whatever :meth:`handle_signature()` returned. 37 | """ 38 | return self.typ + '-' + self.get_id(name, sig) 39 | 40 | def get_entry(self, name, sig): 41 | """ 42 | Returns entry to add for cross-reference IDs. 43 | 44 | *name* is whatever :meth:`handle_signature()` returned. 45 | """ 46 | return name 47 | 48 | def get_id(self, name, sig): 49 | """ 50 | Returns cross-reference ID. 51 | 52 | *name* is whatever :meth:`handle_signature()` returned. 53 | """ 54 | return name 55 | 56 | def add_target_and_index(self, name, sig, signode): 57 | """ 58 | Add cross-reference IDs and entries to self.indexnode, if applicable. 59 | 60 | *name* is whatever :meth:`handle_signature()` returned. 61 | """ 62 | anchor = self.get_anchor(name, sig) 63 | id = self.get_id(name, sig) 64 | self.add_target(anchor=anchor, entry=self.get_entry(name, sig), 65 | id=id, sig=sig, signode=signode) 66 | self.add_index(anchor=anchor, name=name, sig=sig) 67 | 68 | def add_target(self, anchor, id, entry, sig, signode): 69 | """Add cross-references to self.env.domaindata, if applicable.""" 70 | if anchor not in self.state.document.ids: 71 | signode['names'].append(anchor) 72 | signode['ids'].append(anchor) 73 | signode['first'] = (not self.names) 74 | self.state.document.note_explicit_target(signode) 75 | data = self.env.domaindata['http'][self.typ] 76 | if id in data: 77 | otherdocname = data[id][0] 78 | self.env.warn( 79 | self.env.docname, 80 | 'duplicate method description of %s, ' % sig + 81 | 'other instance in ' + 82 | self.env.doc2path(otherdocname) + 83 | ', use :noindex: for one of them', 84 | self.lineno 85 | ) 86 | data[id] = entry 87 | 88 | def add_index(self, anchor, name, sig): 89 | """ 90 | Add index entries to self.indexnode, if applicable. 91 | 92 | *name* is whatever :meth:`handle_signature()` returned. 93 | """ 94 | raise NotImplemented 95 | 96 | 97 | class HTTPMethod(HTTPDescription): 98 | """ 99 | Description of a general HTTP method. 100 | """ 101 | typ = 'method' 102 | nodetype = literal 103 | 104 | option_spec = { 105 | 'noindex': directives.flag, 106 | 'title': directives.unchanged, 107 | 'label-name': directives.unchanged, 108 | } 109 | doc_field_types = [ 110 | TypedField('argument', label=l_('Path arguments'), 111 | names=('arg', 'argument', 'patharg'), 112 | typenames=('argtype', 'pathargtype'), 113 | can_collapse=True), 114 | TypedField('parameter', label=l_('Query params'), 115 | names=('param', 'parameter', 'queryparam'), 116 | typenames=('paramtype', 'queryparamtype'), 117 | typerolename='response', 118 | can_collapse=True), 119 | TypedField('optional_parameter', label=l_('Opt. params'), 120 | names=('optparam', 'optional', 'optionalparameter'), 121 | typenames=('optparamtype',), 122 | can_collapse=True), 123 | TypedField('fragment', label=l_('Fragments'), 124 | names=('frag', 'fragment'), 125 | typenames=('fragtype',), 126 | can_collapse=True), 127 | ResponseField('response', label=l_('Responses'), 128 | names=('resp', 'responds', 'response'), 129 | typerolename='response', 130 | can_collapse=True) 131 | ] 132 | 133 | # RE for HTTP method signatures 134 | sig_re = re.compile( 135 | ( 136 | r'^' 137 | r'(?:(GET|POST|PUT|DELETE)\s+)?' # HTTP method 138 | r'(.+)' # URL 139 | r'\s*$' 140 | ), 141 | re.IGNORECASE 142 | ) 143 | 144 | # Note, path_re.findall() will produce an extra ('', '') tuple 145 | # at the end of its matches. You should strip it off, or you will 146 | path_re = re.compile( 147 | ( 148 | r'([^{]*)' # Plain text 149 | r'(\{[^}]*\})?' # {arg} in matched braces 150 | ), 151 | re.VERBOSE 152 | ) 153 | 154 | def node_from_method(self, method): 155 | """Returns a ``desc_http_method`` Node from a ``method`` string.""" 156 | if method is None: 157 | method = 'GET' 158 | return desc_http_method(method, method.upper()) 159 | 160 | def node_from_url(self, url): 161 | """Returns a ``desc_http_url`` Node from a ``url`` string.""" 162 | if url is None: 163 | raise ValueError 164 | # Split URL into path, query, and fragment 165 | path, query, fragment = self.split_url(url) 166 | urlnode = desc_http_url() 167 | urlnode += self.node_from_path(path) 168 | node = self.node_from_query(query) 169 | if node: 170 | urlnode += node 171 | node = self.node_from_fragment(fragment) 172 | if node: 173 | urlnode += node 174 | return urlnode 175 | 176 | def node_from_path(self, path): 177 | """Returns a ``desc_http_path`` Node from a ``path`` string.""" 178 | if path: 179 | pathnode = desc_http_path(path) 180 | path_segments = self.path_re.findall(path)[:-1] 181 | for text, arg in path_segments: 182 | pathnode += Text(text) 183 | if arg: 184 | arg = arg[1:-1] # Strip off { and } 185 | pathnode += desc_http_patharg(arg, arg) 186 | return pathnode 187 | else: 188 | raise ValueError 189 | 190 | def node_from_query(self, query): 191 | """Returns a ``desc_http_query`` Node from a ``query`` string.""" 192 | if query: 193 | querynode = desc_http_query(query) 194 | query_params = query.split('&') 195 | for p in query_params: 196 | querynode += desc_http_queryparam(p, p) 197 | return querynode 198 | 199 | def node_from_fragment(self, fragment): 200 | """Returns a ``desc_http_fragment`` Node from a ``fragment`` string.""" 201 | if fragment: 202 | return desc_http_fragment(fragment, fragment) 203 | 204 | def split_url(self, url): 205 | """ 206 | Splits a ``url`` string into its components. 207 | Returns (path, query string, fragment). 208 | """ 209 | _, _, path, query, fragment = urlsplit(url) 210 | return (path, query, fragment) 211 | 212 | def handle_signature(self, sig, signode): 213 | """ 214 | Transform an HTTP method signature into RST nodes. 215 | Returns (method name, full URL). 216 | """ 217 | # Match the signature to extract the method and URL 218 | m = self.sig_re.match(sig) 219 | if m is None: 220 | raise ValueError 221 | method, url = m.groups() 222 | # Append nodes to signode for method and url 223 | signode += self.node_from_method(method) 224 | signode += self.node_from_url(url) 225 | # Name and title 226 | name = self.options.get('label-name', 227 | slugify_url(method.lower() + '-' + url)) 228 | title = self.options.get('title', sig) 229 | return (method.upper(), url, name, title) 230 | 231 | def get_entry(self, name, sig): 232 | """ 233 | Returns entry to add for cross-reference IDs. 234 | 235 | *name* is whatever :meth:`handle_signature()` returned. 236 | """ 237 | method, _, _, title = name 238 | return (self.env.docname, sig, title, method) 239 | 240 | def get_id(self, name, sig): 241 | """ 242 | Returns cross-reference ID. 243 | 244 | *name* is whatever :meth:`handle_signature()` returned. 245 | """ 246 | return name[2] 247 | 248 | def add_index(self, anchor, name, sig): 249 | """ 250 | Add index entries to self.indexnode, if applicable. 251 | 252 | *name* is whatever :meth:`handle_signature()` returned. 253 | """ 254 | method, url, id, title = name 255 | if title != sig: 256 | self.indexnode['entries'].append(('single', 257 | _("%s (HTTP method)") % title, 258 | anchor, anchor)) 259 | self.indexnode['entries'].append( 260 | ('single', 261 | _("%(method)s (HTTP method); %(url)s") % {'method': method, 262 | 'url': url}, 263 | anchor, anchor) 264 | ) 265 | 266 | 267 | class HTTPResponse(HTTPDescription): 268 | """ 269 | Description of a general HTTP response. 270 | """ 271 | typ = 'response' 272 | nodetype = strong 273 | 274 | option_spec = { 275 | 'noindex': directives.flag, 276 | } 277 | doc_field_types = [ 278 | TypedField('data', label=l_('Data'), 279 | names=('data',), 280 | typenames=('datatype', 'type'), 281 | typerolename='response', 282 | can_collapse=True), 283 | NoArgGroupedField('contenttype', label=l_('Content Types'), 284 | names=('contenttype', 'mimetype', 'format'), 285 | can_collapse=True), 286 | ] 287 | 288 | def handle_signature(self, sig, signode): 289 | """ 290 | Transform an HTTP response into RST nodes. 291 | Returns the reference name. 292 | """ 293 | name = slugify(sig) 294 | signode += desc_http_response(name, sig) 295 | return name 296 | 297 | def get_entry(self, name, sig): 298 | return (self.env.docname, sig, sig) 299 | 300 | def add_index(self, anchor, name, sig): 301 | """ 302 | Add index entries to self.indexnode, if applicable. 303 | 304 | *name* is whatever :meth:`handle_signature()` returned. 305 | """ 306 | self.indexnode['entries'].append(('single', 307 | _("%s (HTTP response)") % sig, 308 | anchor, anchor)) 309 | self.indexnode['entries'].append(('single', 310 | _("HTTP response; %s") % sig, 311 | anchor, anchor)) 312 | -------------------------------------------------------------------------------- /sphinx_http_domain/docfields.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Fields for the HTTP domain. 4 | """ 5 | 6 | from docutils import nodes 7 | 8 | from sphinx.util.docfields import GroupedField, TypedField 9 | 10 | 11 | class ResponseField(TypedField): 12 | """ 13 | Like a TypedField, but with automatic descriptions. 14 | 15 | Just like a TypedField, you can use a ResponseField with or without 16 | a type:: 17 | 18 | :param 200: description of response 19 | :type 200: SomeObject 20 | 21 | -- or -- 22 | 23 | :param SomeObject 200: description of response 24 | 25 | In addition, ResponseField will provide a default description of 26 | the status code, if you provide none:: 27 | 28 | :param 404: 29 | 30 | -- is equivalent to -- 31 | 32 | :param 404: Not Found 33 | """ 34 | # List of HTTP Status Codes, derived from: 35 | # http://en.wikipedia.org/wiki/List_of_HTTP_status_codes 36 | status_codes = { 37 | # 1xx Informational 38 | '100': 'Continue', 39 | '101': 'Switching Protocols', 40 | '102': 'Processing', 41 | '122': 'Request-URI too long', 42 | # 2xx Success 43 | '200': 'OK', 44 | '201': 'Created', 45 | '202': 'Accepted', 46 | '203': 'Non-Authoritative Information', 47 | '204': 'No Content', 48 | '205': 'Reset Content', 49 | '206': 'Partial Content', 50 | '207': 'Multi-Status', 51 | '226': 'IM Used', 52 | # 3xx Redirection 53 | '300': 'Multiple Choices', 54 | '301': 'Moved Permanently', 55 | '302': 'Found', 56 | '303': 'See Other', 57 | '304': 'Not Modified', 58 | '305': 'Use Proxy', 59 | '306': 'Switch Proxy', 60 | '307': 'Temporary Redirect', 61 | # 4xx Client Error 62 | '400': 'Bad Request', 63 | '401': 'Unauthorized', 64 | '402': 'Payment Requrired', 65 | '403': 'Forbidden', 66 | '404': 'Not Found', 67 | '405': 'Method Not Allowed', 68 | '406': 'Not Acceptable', 69 | '407': 'Proxy Authentication Requried', 70 | '408': 'Request Timeout', 71 | '409': 'Conflict', 72 | '410': 'Gone', 73 | '411': 'Length Required', 74 | '412': 'Precondition Failed', 75 | '413': 'Request Entity Too Large', 76 | '414': 'Request-URI Too Long', 77 | '415': 'Unsupported Media Type', 78 | '416': 'Requested Range Not Satisfiable', 79 | '417': 'Expectation Failed', 80 | '418': "I'm a teapot", 81 | '422': 'Unprocessable Entity', 82 | '423': 'Locked', 83 | '424': 'Failed Dependency', 84 | '425': 'Unordered Collection', 85 | '426': 'Upgrade Required', 86 | '444': 'No Response', 87 | '449': 'Retry With', 88 | '450': 'Block by Windows Parental Controls', 89 | '499': 'Client Closed Request', 90 | # 5xx Server Error 91 | '500': 'Interal Server Error', 92 | '501': 'Not Implemented', 93 | '502': 'Bad Gateway', 94 | '503': 'Service Unavailable', 95 | '504': 'Gateway Timeout', 96 | '505': 'HTTP Version Not Supported', 97 | '506': 'Variant Also Negotiates', 98 | '507': 'Insufficient Storage', 99 | '509': 'Bandwith Limit Exceeded', 100 | '510': 'Not Extended', 101 | } 102 | 103 | def default_content(self, fieldarg): 104 | """ 105 | Given a fieldarg, returns the status code description in list form. 106 | 107 | The default status codes are provided in self.status_codes. 108 | """ 109 | try: 110 | return [nodes.Text(self.status_codes[fieldarg])] 111 | except KeyError: 112 | return [] 113 | 114 | def make_entry(self, fieldarg, content): 115 | # Wrap Field.make_entry, but intercept empty content and replace 116 | # it with default content. 117 | if not content: 118 | content = self.default_content(fieldarg) 119 | return super(TypedField, self).make_entry(fieldarg, content) 120 | 121 | 122 | class NoArgGroupedField(GroupedField): 123 | def __init__(self, *args, **kwargs): 124 | super(NoArgGroupedField, self).__init__(*args, **kwargs) 125 | self.has_arg = False 126 | 127 | def make_field(self, types, domain, items): 128 | if len(items) == 1 and self.can_collapse: 129 | super(NoArgGroupedField, self).make_field(types, domain, items) 130 | fieldname = nodes.field_name('', self.label) 131 | listnode = self.list_type() 132 | for fieldarg, content in items: 133 | par = nodes.paragraph() 134 | par += content 135 | listnode += nodes.list_item('', par) 136 | fieldbody = nodes.field_body('', listnode) 137 | return nodes.field('', fieldname, fieldbody) 138 | -------------------------------------------------------------------------------- /sphinx_http_domain/nodes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Nodes for the HTTP domain. 4 | """ 5 | 6 | from docutils import nodes 7 | 8 | from sphinx.util.texescape import tex_escape_map 9 | 10 | 11 | class HttpNode(nodes.Part, nodes.Inline, nodes.TextElement): 12 | """Generic HTTP node.""" 13 | _writers = ['text', 'html', 'latex', 'man'] 14 | 15 | def set_first(self): 16 | try: 17 | self.children[0].first = True 18 | except IndexError: 19 | pass 20 | 21 | @classmethod 22 | def contribute_to_app(cls, app): 23 | kwargs = {} 24 | for writer in cls._writers: 25 | visit = getattr(cls, 'visit_' + writer, None) 26 | depart = getattr(cls, 'depart_' + writer, None) 27 | if visit and depart: 28 | kwargs[writer] = (visit, depart) 29 | app.add_node(cls, **kwargs) 30 | 31 | @staticmethod 32 | def visit_text(self, node): 33 | pass 34 | 35 | @staticmethod 36 | def depart_text(self, node): 37 | pass 38 | 39 | @staticmethod 40 | def visit_latex(self, node): 41 | pass 42 | 43 | @staticmethod 44 | def depart_latex(self, node): 45 | pass 46 | 47 | @staticmethod 48 | def visit_man(self, node): 49 | pass 50 | 51 | @staticmethod 52 | def depart_man(self, node): 53 | pass 54 | 55 | 56 | class desc_http_method(HttpNode): 57 | """HTTP method node.""" 58 | def astext(self): 59 | return nodes.TextElement.astext(self) + ' ' 60 | 61 | @staticmethod 62 | def depart_text(self, node): 63 | self.add_text(' ') 64 | 65 | @staticmethod 66 | def visit_html(self, node): 67 | self.body.append(self.starttag(node, 'tt', '', 68 | CLASS='descclassname deschttpmethod')) 69 | 70 | @staticmethod 71 | def depart_html(self, node): 72 | self.body.append(' ') 73 | 74 | @staticmethod 75 | def visit_latex(self, node): 76 | self.body.append(r'\code{') 77 | self.literal_whitespace += 1 78 | 79 | @staticmethod 80 | def depart_latex(self, node): 81 | self.body.append(r'}~') 82 | self.literal_whitespace -= 1 83 | 84 | @staticmethod 85 | def depart_man(self, node): 86 | self.body.append(r'\~') 87 | 88 | 89 | class desc_http_url(HttpNode): 90 | """HTTP URL node.""" 91 | @staticmethod 92 | def visit_html(self, node): 93 | self.body.append(self.starttag(node, 'tt', '', 94 | CLASS='descname deschttpurl')) 95 | 96 | @staticmethod 97 | def depart_html(self, node): 98 | self.body.append('') 99 | 100 | @staticmethod 101 | def visit_latex(self, node): 102 | self.body.append(r'\bfcode{') 103 | self.literal_whitespace += 1 104 | 105 | @staticmethod 106 | def depart_latex(self, node): 107 | self.body.append(r'}') 108 | self.literal_whitespace -= 1 109 | 110 | 111 | class desc_http_path(HttpNode): 112 | """HTTP path node. Contained in the URL node.""" 113 | @staticmethod 114 | def visit_html(self, node): 115 | self.body.append(self.starttag(node, 'span', '', 116 | CLASS='deschttppath')) 117 | 118 | @staticmethod 119 | def depart_html(self, node): 120 | self.body.append('') 121 | 122 | 123 | class desc_http_patharg(HttpNode): 124 | """ 125 | HTTP path argument node. Contained in the path node. 126 | 127 | This node is created when {argument} is found inside the path. 128 | """ 129 | wrapper = (u'{', u'}') 130 | 131 | def astext(self, node): 132 | return (self.wrapper[0] + 133 | nodes.TextElement.astext(node) + 134 | self.wrapper[1]) 135 | 136 | @staticmethod 137 | def visit_text(self, node): 138 | self.add_text(node.wrapper[0]) 139 | 140 | @staticmethod 141 | def depart_text(self, node): 142 | self.add_text(node.wrapper[1]) 143 | 144 | @staticmethod 145 | def visit_html(self, node): 146 | self.body.append( 147 | self.starttag(node, 'em', '', CLASS='deschttppatharg') + 148 | self.encode(node.wrapper[0]) + 149 | self.starttag(node, 'span', '', CLASS='deschttppatharg') 150 | ) 151 | 152 | @staticmethod 153 | def depart_html(self, node): 154 | self.body.append('' + 155 | self.encode(node.wrapper[1]) + 156 | '') 157 | 158 | @staticmethod 159 | def visit_latex(self, node): 160 | self.body.append(r'\emph{' + 161 | node.wrapper[0].translate(tex_escape_map)) 162 | 163 | @staticmethod 164 | def depart_latex(self, node): 165 | self.body.append(node.wrapper[1].translate(tex_escape_map) + 166 | '}') 167 | 168 | @staticmethod 169 | def visit_man(self, node): 170 | self.body.append(self.defs['emphasis'][0]) 171 | self.body.append(self.deunicode(node.wrapper[0])) 172 | 173 | @staticmethod 174 | def depart_man(self, node): 175 | self.body.append(self.deunicode(node.wrapper[1])) 176 | self.body.append(self.defs['emphasis'][1]) 177 | 178 | 179 | class desc_http_query(HttpNode): 180 | """HTTP query string node. Contained in the URL node.""" 181 | prefix = u'?' 182 | 183 | def astext(self): 184 | return self.prefix + nodes.TextElement.astext(self) 185 | 186 | @staticmethod 187 | def visit_text(self, node): 188 | self.add_text(node.prefix) 189 | node.set_first() 190 | 191 | @staticmethod 192 | def visit_html(self, node): 193 | self.body.append( 194 | self.starttag(node, 'span', '', CLASS='deschttpquery') + 195 | self.encode(node.prefix) 196 | ) 197 | node.set_first() 198 | 199 | @staticmethod 200 | def depart_html(self, node): 201 | self.body.append('') 202 | 203 | @staticmethod 204 | def visit_latex(self, node): 205 | self.body.append(node.prefix.translate(tex_escape_map)) 206 | node.set_first() 207 | 208 | @staticmethod 209 | def visit_man(self, node): 210 | self.body.append(self.deunicode(node.prefix)) 211 | node.set_first() 212 | 213 | 214 | class desc_http_queryparam(HttpNode): 215 | """ 216 | HTTP query string parameter node. Contained in the query string node. 217 | 218 | This node is created for each parameter inside a query string. 219 | """ 220 | child_text_separator = u'&' 221 | first = False 222 | 223 | @staticmethod 224 | def visit_text(self, node): 225 | if not node.first: 226 | self.add_text(node.child_text_separator) 227 | 228 | @staticmethod 229 | def visit_html(self, node): 230 | if not node.first: 231 | self.body.append(self.encode(node.child_text_separator)) 232 | self.body.append(self.starttag(node, 'em', '', 233 | CLASS='deschttpqueryparam')) 234 | 235 | @staticmethod 236 | def depart_html(self, node): 237 | self.body.append('') 238 | 239 | @staticmethod 240 | def visit_latex(self, node): 241 | if not node.first: 242 | self.body.append( 243 | node.child_text_separator.translate(tex_escape_map) 244 | ) 245 | self.body.append('\emph{') 246 | 247 | @staticmethod 248 | def depart_latex(self, node): 249 | self.body.append('}') 250 | 251 | @staticmethod 252 | def visit_man(self, node): 253 | if not node.first: 254 | self.body.append(self.deunicode(node.child_text_separator)) 255 | self.body.append(self.defs['emphasis'][0]) 256 | 257 | @staticmethod 258 | def depart_man(self, node): 259 | self.body.append(self.defs['emphasis'][1]) 260 | 261 | 262 | class desc_http_fragment(HttpNode): 263 | """HTTP fragment node. Contained in the URL node.""" 264 | prefix = u'#' 265 | 266 | def astext(self): 267 | return self.prefix + nodes.TextElement.astext(self) 268 | 269 | @staticmethod 270 | def visit_text(self, node): 271 | self.add_text(node.prefix) 272 | 273 | @staticmethod 274 | def visit_html(self, node): 275 | self.body.append(self.encode(node.prefix) + 276 | self.starttag(node, 'em', '', 277 | CLASS='deschttpfragment')) 278 | 279 | @staticmethod 280 | def depart_html(self, node): 281 | self.body.append('') 282 | 283 | @staticmethod 284 | def visit_latex(self, node): 285 | self.body.append(node.prefix.translate(tex_escape_map) + 286 | r'\emph{') 287 | 288 | @staticmethod 289 | def depart_latex(self, node): 290 | self.body.append('}') 291 | 292 | @staticmethod 293 | def visit_man(self, node): 294 | self.body.append(self.deunicode(node.prefix)) 295 | self.body.append(self.defs['emphasis'][0]) 296 | 297 | @staticmethod 298 | def depart_man(self, node): 299 | self.body.append(self.defs['emphasis'][1]) 300 | 301 | 302 | class desc_http_response(HttpNode): 303 | """HTTP response node.""" 304 | 305 | @staticmethod 306 | def visit_html(self, node): 307 | self.body.append(self.starttag(node, 'strong', '', 308 | CLASS='deschttpresponse')) 309 | 310 | @staticmethod 311 | def depart_html(self, node): 312 | self.body.append('') 313 | 314 | @staticmethod 315 | def visit_latex(self, node): 316 | self.body.append(r'\textbf{') 317 | 318 | @staticmethod 319 | def depart_latex(self, node): 320 | self.body.append('}') 321 | 322 | @staticmethod 323 | def visit_man(self, node): 324 | self.body.append(self.defs['strong'][0]) 325 | 326 | @staticmethod 327 | def depart_man(self, node): 328 | self.body.append(self.defs['strong'][1]) 329 | -------------------------------------------------------------------------------- /sphinx_http_domain/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | sphinx.domains.http 4 | ~~~~~~~~~~~~~~~~~~~ 5 | 6 | Utilities for the HTTP domain. 7 | """ 8 | 9 | import re 10 | import unicodedata 11 | 12 | 13 | _slugify_strip_re = re.compile(r'[^\w\s-]') 14 | _slugify_strip_url_re = re.compile(r'[^\w\s/?=&#;{}-]') 15 | _slugify_hyphenate_re = re.compile(r'[^\w]+') 16 | 17 | 18 | def slugify(value, strip_re=_slugify_strip_re): 19 | """ 20 | Normalizes string, converts to lowercase, removes non-alpha 21 | characters, and converts spaces to hyphens. 22 | 23 | From Django's "django/template/defaultfilters.py". 24 | """ 25 | if not isinstance(value, unicode): 26 | value = unicode(value) 27 | value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') 28 | value = unicode(strip_re.sub('', value).strip().lower()) 29 | return _slugify_hyphenate_re.sub('-', value) 30 | 31 | 32 | def slugify_url(value): 33 | """ 34 | Normalizes URL, converts to lowercase, removes non-URL 35 | characters, and converts non-alpha characters to hyphens. 36 | """ 37 | return slugify(value, strip_re=_slugify_strip_url_re) 38 | --------------------------------------------------------------------------------