From 49c28aaa980ffc88a49924086ee5ccbfca133b77 Mon Sep 17 00:00:00 2001 From: Philip James Elson Date: Mon, 3 May 2021 11:33:17 +0000 Subject: [PATCH] Implement common markdown blocks infrastructure --- cara/apps/calculator/__init__.py | 5 ++ cara/apps/calculator/__main__.py | 5 +- cara/apps/calculator/markdown_tools.py | 73 +++++++++++++++++++ cara/apps/templates/common_text.md.j2 | 0 .../apps/calculator/test_markdown_tools.py | 30 ++++++++ cara/tests/apps/calculator/test_webapp.py | 5 ++ setup.py | 10 +-- 7 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 cara/apps/calculator/markdown_tools.py create mode 100644 cara/apps/templates/common_text.md.j2 create mode 100644 cara/tests/apps/calculator/test_markdown_tools.py diff --git a/cara/apps/calculator/__init__.py b/cara/apps/calculator/__init__.py index 25c680a0..12c8a624 100644 --- a/cara/apps/calculator/__init__.py +++ b/cara/apps/calculator/__init__.py @@ -12,6 +12,7 @@ import zlib import jinja2 from tornado.web import Application, RequestHandler, StaticFileHandler +from . import markdown_tools from . import model_generator from .report_generator import ReportGenerator from .user import AuthenticatedUser, AnonymousUser @@ -183,6 +184,10 @@ def make_app( loader=loader, ) + template_environment.globals['common_text'] = markdown_tools.extract_rendered_markdown_blocks( + template_environment.get_template('common_text.md.j2') + ) + return Application( urls, debug=debug, diff --git a/cara/apps/calculator/__main__.py b/cara/apps/calculator/__main__.py index 1b9bcce2..0cfafe66 100644 --- a/cara/apps/calculator/__main__.py +++ b/cara/apps/calculator/__main__.py @@ -6,7 +6,7 @@ from tornado.ioloop import IOLoop from . import make_app -def configure_parser(parser): +def configure_parser(parser) -> argparse.ArgumentParser: parser.add_argument( "--no-debug", help="Don't enable debug mode", action="store_false", @@ -20,8 +20,7 @@ def configure_parser(parser): def main(): - parser = argparse.ArgumentParser() - configure_parser(parser) + parser = configure_parser(argparse.ArgumentParser()) args = parser.parse_args() theme_dir = args.theme if theme_dir is not None: diff --git a/cara/apps/calculator/markdown_tools.py b/cara/apps/calculator/markdown_tools.py new file mode 100644 index 00000000..ba964d5c --- /dev/null +++ b/cara/apps/calculator/markdown_tools.py @@ -0,0 +1,73 @@ +import re + +import jinja2 +import mistune + + +HEADER_PATTERN = re.compile(r'\n(#+)(.*)') + + +def _block_headings(contents: str): + """ + Return the headings (and the start/end positions of their blocks) of + markdown, in reverse order. + + Note that a block ends when the next heading is found, even if that heading + is a sub-block of the current one. + + """ + all_block_headings = HEADER_PATTERN.finditer( + '\n' + contents, re.MULTILINE & re.DOTALL + ) + + end_pos = None + for result in list(all_block_headings)[::-1]: + heading = { + 'start_pos': result.end(), + 'end_pos': end_pos, + 'depth': len(result[1]), + 'heading': result[2].strip(), + } + end_pos = result.start() + yield heading + + +def extract_block(block_name: str, contents: str) -> str: + """ + Extract the given header block from the given markdown. + + The result *does not* contain the children headers of the block. + + """ + for block in _block_headings(contents): + if block['heading'] == block_name: + return contents[block['start_pos']: block['end_pos']] + else: + raise ValueError(f"Heading \"{ block_name }\" not found") + + +def extract_headings(contents: str) -> list: + """ + Extract all headers from the given markdown. + + """ + headings = [] + for block in _block_headings(contents): + headings.append(block['heading']) + return headings + + +def extract_rendered_markdown_blocks(template: jinja2.Template) -> dict: + """ + Return a dictionary of all common markdown text blocks, rendered to HTML and + uniquely identified by their headings, from the given Jinja2 template. + + """ + common_text = template.render() + headings = extract_headings(common_text) + text_blocks = {} + for heading in headings: + block = extract_block(heading, common_text) + html_content = mistune.markdown(block, escape=False) + text_blocks[heading] = html_content + return text_blocks diff --git a/cara/apps/templates/common_text.md.j2 b/cara/apps/templates/common_text.md.j2 new file mode 100644 index 00000000..e69de29b diff --git a/cara/tests/apps/calculator/test_markdown_tools.py b/cara/tests/apps/calculator/test_markdown_tools.py new file mode 100644 index 00000000..8b913067 --- /dev/null +++ b/cara/tests/apps/calculator/test_markdown_tools.py @@ -0,0 +1,30 @@ +import textwrap + +import jinja2 +import pytest + +import cara.apps.calculator.markdown_tools as md_tools + + +@pytest.fixture +def example_template(): + return jinja2.Environment().from_string(textwrap.dedent(""" + # A header + + Some *text* + + {% block using_jinja_blocks %} + # Another header + + Some more **text**. + {% endblock %} + + """)) + + +def test_extract_blocks(example_template): + blocks = md_tools.extract_rendered_markdown_blocks(example_template) + assert 'A header' in blocks + assert blocks['A header'] == '

Some text

\n' + assert 'Another header' in blocks + assert blocks['Another header'] == '

Some more text.

\n' diff --git a/cara/tests/apps/calculator/test_webapp.py b/cara/tests/apps/calculator/test_webapp.py index 97589339..3c133684 100644 --- a/cara/tests/apps/calculator/test_webapp.py +++ b/cara/tests/apps/calculator/test_webapp.py @@ -26,6 +26,11 @@ async def test_calculator_form(http_server_client): assert response.code == 200 +async def test_user_guide(http_server_client): + resp = await http_server_client.fetch('/calculator/user-guide') + assert resp.code == 200 + + class TestBasicApp(tornado.testing.AsyncHTTPTestCase): def get_app(self): return cara.apps.calculator.make_app() diff --git a/setup.py b/setup.py index ab2a021a..7e5b77ec 100644 --- a/setup.py +++ b/setup.py @@ -73,11 +73,9 @@ setup( 'all': [req for reqs in REQUIREMENTS.values() for req in reqs], }, package_data={'cara': [ - 'apps/templates/*.j2', - 'apps/calculator/templates/*.j2', - 'apps/calculator/*', - 'apps/calculator/*/*', - 'apps/calculator/*/*/*', - 'apps/calculator/*/*/*/*', + 'apps/*/*', + 'apps/*/*/*', + 'apps/*/*/*/*', + 'apps/*/*/*/*/*', ]}, )