Skip to content

Commit

Permalink
Can use with other tab libraries
Browse files Browse the repository at this point in the history
Added a :http:example-block: directive for translating and rendering
a HTTP request or response anywhere,
including in tab directives from other Sphinx extensions.

Closes collective#25
  • Loading branch information
AWhetter committed Aug 16, 2024
1 parent 8ec736b commit 49a35fd
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 91 deletions.
4 changes: 3 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['sphinxcontrib.httpdomain',
extensions = ['sphinx_design',
'sphinx_inline_tabs',
'sphinxcontrib.httpdomain',
'sphinxcontrib.httpexample']

try:
Expand Down
68 changes: 68 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,74 @@ The audience for this extension are developers and technical writes documenting
Accept: application/json
Authorization: Basic YWRtaW46YWRtaW4=

* Compatible with other tab libraries:

`sphinx-inline-tabs <https://sphinx-inline-tabs.readthedocs.io/en/latest/>`_:

.. tab:: http

.. http:example-block:: http
:request: ../tests/fixtures/001.request.txt

.. tab:: curl

.. http:example-block:: curl
:request: ../tests/fixtures/001.request.txt

.. tab:: wget

.. http:example-block:: wget
:request: ../tests/fixtures/001.request.txt

.. tab:: httpie

.. http:example-block:: httpie
:request: ../tests/fixtures/001.request.txt

.. tab:: python-requests

.. http:example-block:: wget
:request: ../tests/fixtures/001.request.txt

.. tab:: response

.. http:example-block:: response
:response: ../tests/fixtures/001.response.txt

`sphinx-design <https://sphinx-design.readthedocs.io/en/furo-theme/tabs.html>`_:

.. tab-set::

.. tab-item:: http

.. http:example-block:: http
:request: ../tests/fixtures/001.request.txt

.. tab-item:: curl

.. http:example-block:: curl
:request: ../tests/fixtures/001.request.txt

.. tab-item:: wget

.. http:example-block:: wget
:request: ../tests/fixtures/001.request.txt

.. tab-item:: httpie

.. http:example-block:: httpie
:request: ../tests/fixtures/001.request.txt

.. tab-item:: python-requests

.. http:example-block:: wget
:request: ../tests/fixtures/001.request.txt

.. tab-item:: response

.. http:example-block:: response
:response: ../tests/fixtures/001.response.txt

* Supported tools:

- curl_
Expand Down
2 changes: 2 additions & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pyyaml
rst2pdf
snapshottest==0.5.0
sphinx
sphinx-design
sphinx-inline-tabs
sphinx-testing
sphinx_rtd_theme
sphinxcontrib-httpdomain
Expand Down
3 changes: 2 additions & 1 deletion src/sphinxcontrib/httpexample/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from sphinxcontrib.httpexample.directives import HTTPExample
from sphinxcontrib.httpexample.directives import HTTPExample, HTTPExampleBlock

import os
import pkg_resources
Expand Down Expand Up @@ -28,6 +28,7 @@ def copy_assets(app, exception):
def setup(app):
app.connect('build-finished', copy_assets)
app.add_directive_to_domain('http', 'example', HTTPExample)
app.add_directive_to_domain('http', 'example-block', HTTPExampleBlock)
app.add_js_file(JS_FILE)
app.add_css_file(CSS_FILE)
app.add_config_value('httpexample_scheme', 'http', 'html')
Expand Down
208 changes: 119 additions & 89 deletions src/sphinxcontrib/httpexample/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,50 @@ def choose_builders(arguments):
for argument in (arguments or [])]


def process_content(content):
raw = ('\r\n'.join(content)).encode('utf-8')
request = parsers.parse_request(raw)
params, _ = request.extract_fields('query')
params = [(p[1], p[2]) for p in params]
new_path = utils.add_url_params(request.path, params)
content[0] = ' '.join([request.command, new_path, request.request_version])

# split the request and optional response in the content.
# The separator is two empty lines followed by a line starting with
# 'HTTP/' or 'HTTP '
request_content = StringList()
request_content_no_fields = StringList()
response_content = None
emptylines_count = 0
in_response = False
is_field = r':({}) (.+): (.+)'.format('|'.join(AVAILABLE_FIELDS))
for i, line in enumerate(content):
source = content.source(i)
if in_response:
response_content.append(line, source)
else:
if emptylines_count >= 2 and \
(line.startswith('HTTP/') or line.startswith('HTTP ')):
in_response = True
response_content = StringList()
response_content.append(line, source)
elif line == '':
emptylines_count += 1
else:
request_content.extend(
StringList([''] * emptylines_count, source))
request_content.append(line, source)

if not re.match(is_field, line):
request_content_no_fields.extend(
StringList([''] * emptylines_count, source))
request_content_no_fields.append(line, source)

emptylines_count = 0

return (request_content, request_content_no_fields, response_content)


class HTTPExample(CodeBlock):

required_arguments = 0
Expand All @@ -42,110 +86,49 @@ class HTTPExample(CodeBlock):
})

def run(self):
config = self.state.document.settings.env.config

# Read enabled builders; Defaults to None
chosen_builders = choose_builders(self.arguments)

# Enable 'http' language for http part
self.arguments = ['http']

# process 'query' reST fields
if self.content:
raw = ('\r\n'.join(self.content)).encode('utf-8')
request = parsers.parse_request(raw)
params, _ = request.extract_fields('query')
params = [(p[1], p[2]) for p in params]
new_path = utils.add_url_params(request.path, params)
self.content[0] = ' '.join(
[request.command, new_path, request.request_version])

# split the request and optional response in the content.
# The separator is two empty lines followed by a line starting with
# 'HTTP/' or 'HTTP '
request_content = StringList()
request_content_no_fields = StringList()
response_content = None
emptylines_count = 0
in_response = False
is_field = r':({}) (.+): (.+)'.format('|'.join(AVAILABLE_FIELDS))
for i, line in enumerate(self.content):
source = self.content.source(i)
if in_response:
response_content.append(line, source)
else:
if emptylines_count >= 2 and \
(line.startswith('HTTP/') or line.startswith('HTTP ')):
in_response = True
response_content = StringList()
response_content.append(line, source)
elif line == '':
emptylines_count += 1
else:
request_content.extend(
StringList([''] * emptylines_count, source))
request_content.append(line, source)

if not re.match(is_field, line):
request_content_no_fields.extend(
StringList([''] * emptylines_count, source))
request_content_no_fields.append(line, source)

emptylines_count = 0

# Load optional external request
cwd = os.path.dirname(self.state.document.current_source)
if 'request' in self.options:
request = utils.resolve_path(self.options['request'], cwd)
with open(request) as fp:
request_content = request_content_no_fields = StringList(
list(map(str.rstrip, fp.readlines())), request)

# Load optional external response
if 'response' in self.options:
response = utils.resolve_path(self.options['response'], cwd)
with open(response) as fp:
response_content = StringList(
list(map(str.rstrip, fp.readlines())), response)

# reset the content to the request, stripped of the reST fields
self.content = request_content_no_fields
response_content = StringList()
if self.content:
_, request_content_no_fields, response_content = process_content(StringList(self.content))
have_request = bool(request_content_no_fields) or 'request' in self.options
have_response = bool(response_content) or 'response' in self.options

# Wrap and render main directive as 'http-example-http'
klass = 'http-example-http'
container = nodes.container('', classes=[klass])
container.append(nodes.caption('', 'http'))
container.extend(super(HTTPExample, self).run())
block = HTTPExampleBlock(
'http:example-block',
['http'],
self.options,
self.content,
self.lineno,
self.content_offset,
self.block_text,
self.state,
self.state_machine
)
container.extend(block.run())

# Init result node list
result = [container]

# reset the content to just the request
self.content = request_content

# Append builder responses
if request_content_no_fields:
raw = ('\r\n'.join(request_content_no_fields)).encode('utf-8')
for name in chosen_builders:
request = parsers.parse_request(raw, config.httpexample_scheme)
builder_, language = AVAILABLE_BUILDERS[name]

if have_request:
for argument in self.arguments:
name = argument
# Setting plone JavaScript tab name
name = 'JavaScript' if name == 'plone-javascript' else name

command = builder_(request)

content = StringList(
[command], request_content_no_fields.source(0))
options = self.options.copy()
options.pop('name', None)
options.pop('caption', None)

block = CodeBlock(
'code-block',
[language],
block = HTTPExampleBlock(
'http:example-block',
[argument],
options,
content,
self.content,
self.lineno,
self.content_offset,
self.block_text,
Expand All @@ -163,16 +146,16 @@ def run(self):
result.append(container)

# Append optional response
if response_content:
if have_response:
options = self.options.copy()
options.pop('name', None)
options.pop('caption', None)

block = CodeBlock(
'code-block',
block = HTTPExampleBlock(
'http:example-block',
['http'],
options,
response_content,
self.content,
self.lineno,
self.content_offset,
self.block_text,
Expand All @@ -194,3 +177,50 @@ def run(self):
container_node.extend(result)

return [container_node]


class HTTPExampleBlock(CodeBlock):
required_arguments = 1

option_spec = utils.merge_dicts(CodeBlock.option_spec, {
'request': directives.unchanged,
'response': directives.unchanged,
})

def read_http_file(self, path):
cwd = os.path.dirname(self.state.document.current_source)
request = utils.resolve_path(path, cwd)
with open(request) as fp:
return StringList(list(map(str.rstrip, fp.readlines())), request)

def run(self):
if self.arguments == ['http']:
if 'request' in self.options:
self.content = self.read_http_file(self.options['request'])
else:
self.content = process_content(self.content)[1]
elif self.arguments == ['response']:
if 'response' in self.options:
self.content = self.read_http_file(self.options['response'])
else:
self.content = process_content(self.content)[2]

self.arguments = ['http']
else:
if 'request' in self.options:
request_content_no_fields = self.read_http_file(self.options['request'])
else:
request_content_no_fields = process_content(self.content)[1]

raw = ('\r\n'.join(request_content_no_fields)).encode('utf-8')

config = self.env.config
request = parsers.parse_request(raw, config.httpexample_scheme)
name = choose_builders(self.arguments)[0]
builder_, language = AVAILABLE_BUILDERS[name]
self.arguments = [language]

command = builder_(request)
self.content = StringList([command], request_content_no_fields.source(0))

return super(HTTPExampleBlock, self).run()

0 comments on commit 49a35fd

Please sign in to comment.