Compare commits

..

No commits in common. "985bb9383954f0ec3773a8f8863ef4484bedc1fe" and "853a58b78b687dd9549e0a12e3eb630a0baa053d" have entirely different histories.

16 changed files with 117 additions and 206 deletions

View File

@ -32,9 +32,10 @@ class Config(object):
}, },
} }
MARKDOWN_EXTENSIONS = ['extra', 'incorporealcms.mdx.figures', 'sane_lists', 'smarty', 'toc'] MARKDOWN_EXTENSIONS = ['extra', 'sane_lists', 'smarty', 'tables']
MARKDOWN_EXTENSION_CONFIGS = { MARKDOWN_EXTENSION_CONFIGS = {
'extra': { 'extra': {
'attr_list': {},
'footnotes': { 'footnotes': {
'UNIQUE_IDS': True, 'UNIQUE_IDS': True,
}, },

View File

@ -1,60 +0,0 @@
"""Create generic figures with captions."""
import re
from xml.etree.ElementTree import SubElement
import markdown
class FigureExtension(markdown.Extension):
"""Wrap the markdown prepcoressor."""
def extendMarkdown(self, md):
"""Add FigureBlockProcessor to the Markdown instance."""
md.parser.blockprocessors.register(FigureBlockProcessor(md.parser), 'figure', 100)
class FigureBlockProcessor(markdown.blockprocessors.BlockProcessor):
"""Process figures."""
# |> thing to put in the figure
# |: optional caption for the figure
# optional whatever else, like maybe an attr_list
figure_regex = re.compile(r'^[ ]{0,3}\|>[ ]{0,3}(?P<content>[^\n]*)')
caption_regex = re.compile(r'^[ ]{0,3}\|:[ ]{0,3}(?P<caption>[^\n]*)')
def test(self, parent, block):
"""Determine if we should process this block."""
lines = block.split('\n')
return bool(self.figure_regex.search(lines[0]))
def run(self, parent, blocks):
"""Replace the top block with HTML."""
block = blocks.pop(0)
lines = block.split('\n')
# consume line and create a figure
figure_match = self.figure_regex.search(lines[0])
lines.pop(0)
content = figure_match.group('content')
figure = SubElement(parent, 'figure')
figure.text = content
if lines:
if caption_match := self.caption_regex.search(lines[0]):
# consume line and add the caption as a child of the figure
lines.pop(0)
caption = caption_match.group('caption')
figcaption = SubElement(figure, 'figcaption')
figcaption.text = caption
if lines:
# other lines are mysteries, might be attr_list, so re-append
# make sure there's a child to hang the rest (which is maybe an attr_list?) off of
# this is probably a bad hack
if not len(list(figure)):
SubElement(figure, 'span')
rest = '\n'.join(lines)
figure[-1].tail = f'\n{rest}'
def makeExtension(*args, **kwargs):
"""Provide the extension to the markdown extension loader."""
return FigureExtension(*args, **kwargs)

View File

@ -20,17 +20,14 @@ class InlinePydot(markdown.Extension):
class InlinePydotPreprocessor(markdown.preprocessors.Preprocessor): class InlinePydotPreprocessor(markdown.preprocessors.Preprocessor):
"""Identify dot codeblocks and run them through pydot.""" """Identify dot codeblocks and run them through pydot."""
BLOCK_RE = re.compile(r'~~~{\s+pydot:(?P<filename>[^\s]+)\s+}\n(?P<content>.*?)~~~', re.DOTALL) BLOCK_RE = re.compile(r'~~~pydot:(?P<filename>[^\s]+)\n(?P<content>.*?)~~~', re.DOTALL)
def run(self, lines): def run(self, lines):
"""Match and generate diagrams from dot code blocks.""" """Match and generate diagrams from dot code blocks."""
text = '\n'.join(lines) text = '\n'.join(lines)
out = text for match in self.BLOCK_RE.finditer(text):
for block_match in self.BLOCK_RE.finditer(text): filename = match.group(1)
filename = block_match.group(1) dot_string = match.group(2)
dot_string = block_match.group(2)
logger.debug("matched markdown block: %s", dot_string)
logger.debug("match start/end: %s/%s", block_match.start(), block_match.end())
# use pydot to turn the text into pydot # use pydot to turn the text into pydot
graphs = pydot.graph_from_dot_data(dot_string) graphs = pydot.graph_from_dot_data(dot_string)
@ -44,9 +41,9 @@ class InlinePydotPreprocessor(markdown.preprocessors.Preprocessor):
inline_image = f'![{filename}]({data_path})' inline_image = f'![{filename}]({data_path})'
# replace the image in the output markdown # replace the image in the output markdown
out = out.replace(block_match.group(0), inline_image) text = f'{text[:match.start()]}\n{inline_image}\n{text[match.end():]}'
return out.split('\n') return text.split('\n')
def makeExtension(*args, **kwargs): def makeExtension(*args, **kwargs):

View File

@ -12,11 +12,11 @@ body {
} }
.site-wrap-normal-width { .site-wrap-normal-width {
max-width: 80pc; max-width: 70pc;
} }
.site-wrap-double-width { .site-wrap-double-width {
max-width: 160pc; max-width: 140pc;
} }
.site-wrap { .site-wrap {
@ -26,7 +26,22 @@ body {
margin-right: auto; margin-right: auto;
} }
a { a:link {
font-weight: bold;
text-decoration: none;
}
a:visited {
font-weight: bold;
text-decoration: none;
}
a:hover {
font-weight: bold;
text-decoration: none;
}
a:active {
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
} }
@ -35,7 +50,7 @@ div.header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
font-size: 0.75em; font-size: 0.75em;
padding: 0.5em 1em; padding: 0.25em 0.5em;
} }
div.header a { div.header a {
@ -45,18 +60,7 @@ div.header a {
div.content { div.content {
font-size: 11pt; font-size: 11pt;
padding: 0 1em; padding: 0 1em;
line-height: 1.6em; line-height: 1.5em;
}
@media only screen and (min-width: 70pc) {
div.content, footer {
margin-left: 5pc;
margin-right: 5pc;
}
}
div.content p {
margin: 1.25em 0;
} }
sup, sub { sup, sub {
@ -75,7 +79,7 @@ footer {
color: #999; color: #999;
padding: 0 1em; padding: 0 1em;
padding-bottom: 16px; padding-bottom: 16px;
margin-top: 30px; margin-top: 15px;
} }
table { table {
@ -131,26 +135,39 @@ img {
margin-left: 1em; margin-left: 1em;
} }
figure { .img-frame {
padding: 5px; padding: 5px;
margin: 0;
margin-bottom: 5px;
text-align: center;
max-width: 30%;
min-width: 10em;
display: inline-block;
} }
figure.right { /* For screens with width smaller than 400px */
float: right; .figure-left .figure-right {
max-width: 95%;
float: none;
margin-left: 10px; margin-left: 10px;
display: block; margin-right: 10px;
} }
figure.left { /* For larger screens */
@media only screen and (min-width: 400px) {
.figure-left {
float: left; float: left;
margin-right: 10px; margin-top: 0;
display: block; margin-left: 0;
}
.figure-right {
float: right;
margin-top: 0;
margin-right: 0;
}
}
figure {
max-width: 400px;
padding: 5px;
margin: 10px;
margin-top: 0;
margin-bottom: 5px;
} }
figure img { figure img {
@ -159,8 +176,19 @@ figure img {
} }
figcaption { figcaption {
font-family: "Times New Roman", serif;
color: #777777;
text-align: center; text-align: center;
font-size: 0.9em; font-style: italic;
line-height: 1.3em;
margin-top: 5px;
}
.thumbnail-image {
width: 150px;
height: auto;
margin: 5px;
display: inline;
} }
.footnote { .footnote {

View File

@ -15,15 +15,24 @@ strong {
.site-wrap { .site-wrap {
background: black; background: black;
border: 1px solid #222;
border-top: none;
border-bottom: none;
}
h1, h2, h3, h4, h5, h6 {
color: #B31D15;
} }
a:link, a:visited { a:link, a:visited {
color: #B31D15; color: #EEE;
border-bottom: 1px dotted #EEE;
} }
a:hover, a:active { a:hover, a:active {
color: #B31D15; color: #B31D15;
border-bottom: 1px solid #B31D15; border-bottom: 1px dotted #B31D15;
} }
div.header { div.header {
@ -49,11 +58,7 @@ blockquote {
border: 1px solid #222; border: 1px solid #222;
} }
figure { .img-frame {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
border: 1px solid #333; border: 1px solid #333;
} }
figcaption {
color: #AAAAAA;
}

View File

@ -15,15 +15,24 @@ strong {
.site-wrap { .site-wrap {
background: white; background: white;
border: 1px solid #DDD;
border-top: none;
border-bottom: none;
}
h1, h2, h3, h4, h5, h6 {
color: #811610;
} }
a:link, a:visited { a:link, a:visited {
color: #811610; color: #111;
border-bottom: 1px dotted #111;
} }
a:hover, a:active { a:hover, a:active {
color: #811610; color: #811610;
border-bottom: 1px solid #B31D15; border-bottom: 1px dotted #811610;
} }
div.header { div.header {
@ -49,11 +58,7 @@ blockquote {
border: 1px solid #DDD; border: 1px solid #DDD;
} }
figure { .img-frame {
background-color: rgba(0, 0, 0, 0.1); background-color: rgba(0, 0, 0, 0.1);
border: 1px solid #CCC; border: 1px solid #CCC;
} }
figcaption {
color: #666666;
}

View File

@ -6,7 +6,7 @@ pytest
pytest-cov pytest-cov
# linting and other static code analysis # linting and other static code analysis
bandit bandit==1.6.2 # pinned because 1.7.0 wasn't running right in tox
dlint dlint
flake8 flake8
flake8-blind-except flake8-blind-except

View File

@ -6,13 +6,13 @@
# #
attrs==21.4.0 attrs==21.4.0
# via pytest # via pytest
bandit==1.7.4 bandit==1.6.2
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
certifi==2021.10.8 certifi==2021.10.8
# via requests # via requests
charset-normalizer==2.0.12 charset-normalizer==2.0.12
# via requests # via requests
click==8.1.2 click==8.0.4
# via # via
# flask # flask
# pip-tools # pip-tools
@ -54,7 +54,7 @@ flake8-logging-format==0.6.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-mutable==1.2.0 flake8-mutable==1.2.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flask==2.1.1 flask==2.0.3
# via -r requirements/requirements.in # via -r requirements/requirements.in
gitdb==4.0.9 gitdb==4.0.9
# via gitpython # via gitpython
@ -68,7 +68,7 @@ isort==5.10.1
# via flake8-isort # via flake8-isort
itsdangerous==2.1.2 itsdangerous==2.1.2
# via flask # via flask
jinja2==3.1.1 jinja2==3.1.0
# via flask # via flask
markdown==3.3.6 markdown==3.3.6
# via -r requirements/requirements.in # via -r requirements/requirements.in
@ -86,9 +86,9 @@ pbr==5.8.1
# via stevedore # via stevedore
pep517==0.12.0 pep517==0.12.0
# via pip-tools # via pip-tools
pip-tools==6.6.0 pip-tools==6.5.1
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
platformdirs==2.5.2 platformdirs==2.5.1
# via virtualenv # via virtualenv
pluggy==1.0.0 pluggy==1.0.0
# via # via
@ -106,7 +106,7 @@ pydot==1.4.2
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
pyflakes==2.4.0 pyflakes==2.4.0
# via flake8 # via flake8
pyparsing==3.0.8 pyparsing==3.0.7
# via # via
# packaging # packaging
# pydot # pydot
@ -126,6 +126,7 @@ safety==1.10.3
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
six==1.16.0 six==1.16.0
# via # via
# bandit
# tox # tox
# virtualenv # virtualenv
smmap==5.0.0 smmap==5.0.0
@ -145,7 +146,7 @@ tomli==2.0.1
# coverage # coverage
# pep517 # pep517
# pytest # pytest
tox==3.25.0 tox==3.24.5
# via # via
# -r requirements/requirements-dev.in # -r requirements/requirements-dev.in
# tox-wheel # tox-wheel
@ -155,9 +156,9 @@ urllib3==1.26.9
# via requests # via requests
versioneer==0.22 versioneer==0.22
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
virtualenv==20.14.1 virtualenv==20.13.4
# via tox # via tox
werkzeug==2.1.1 werkzeug==2.0.3
# via flask # via flask
wheel==0.37.1 wheel==0.37.1
# via # via

View File

@ -4,17 +4,17 @@
# #
# pip-compile --output-file=requirements/requirements.txt requirements/requirements.in # pip-compile --output-file=requirements/requirements.txt requirements/requirements.in
# #
click==8.1.2 click==8.0.4
# via flask # via flask
flask==2.1.1 flask==2.0.3
# via -r requirements/requirements.in # via -r requirements/requirements.in
itsdangerous==2.1.2 itsdangerous==2.1.2
# via flask # via flask
jinja2==3.1.1 jinja2==3.1.0
# via flask # via flask
markdown==3.3.6 markdown==3.3.6
# via -r requirements/requirements.in # via -r requirements/requirements.in
markupsafe==2.1.1 markupsafe==2.1.1
# via jinja2 # via jinja2
werkzeug==2.1.1 werkzeug==2.0.3
# via flask # via flask

View File

@ -29,17 +29,6 @@ def test_graphviz_is_rendered():
assert b'data:image/png;base64' in response.data assert b'data:image/png;base64' in response.data
def test_two_graphviz_are_rendered():
"""Test two images are rendered."""
app = app_with_pydot()
client = app.test_client()
response = client.get('/test-two-graphviz')
assert response.status_code == 200
assert b'~~~pydot' not in response.data
assert b'data:image/png;base64' in response.data
def test_invalid_graphviz_is_not_rendered(): def test_invalid_graphviz_is_not_rendered():
"""Check that invalid graphviz doesn't blow things up.""" """Check that invalid graphviz doesn't blow things up."""
app = app_with_pydot() app = app_with_pydot()
@ -48,16 +37,3 @@ def test_invalid_graphviz_is_not_rendered():
response = client.get('/test-invalid-graphviz') response = client.get('/test-invalid-graphviz')
assert response.status_code == 500 assert response.status_code == 500
assert b'INTERNAL SERVER ERROR' in response.data assert b'INTERNAL SERVER ERROR' in response.data
def test_figures_are_rendered(client):
"""Test that a page with my figure syntax renders as expected."""
response = client.get('/figures')
assert response.status_code == 200
assert (b'<figure class="right"><img alt="fancy captioned logo" src="bss-square-no-bg.png" />'
b'<figcaption>this is my cool logo!</figcaption></figure>') in response.data
assert (b'<figure><img alt="vanilla captioned logo" src="bss-square-no-bg.png" />'
b'<figcaption>this is my cool logo without an attr!</figcaption>\n</figure>') in response.data
assert (b'<figure class="left"><img alt="fancy logo" src="bss-square-no-bg.png" />'
b'<span></span></figure>') in response.data
assert b'<figure><img alt="just a logo" src="bss-square-no-bg.png" /></figure>' in response.data

View File

@ -7,7 +7,7 @@ def test_page_that_exists(client):
"""Test that the app can serve a basic file at the index.""" """Test that the app can serve a basic file at the index."""
response = client.get('/') response = client.get('/')
assert response.status_code == 200 assert response.status_code == 200
assert b'<h1 id="test-index">test index</h1>' in response.data assert b'<h1>test index</h1>' in response.data
def test_direct_file_that_exists(client): def test_direct_file_that_exists(client):
@ -134,32 +134,32 @@ def test_that_page_request_redirects_to_directory(client):
""" """
response = client.get('/subdir') response = client.get('/subdir')
assert response.status_code == 301 assert response.status_code == 301
assert response.location == '/subdir/' assert response.location == 'http://localhost/subdir/'
def test_that_request_to_symlink_redirects_markdown(client): def test_that_request_to_symlink_redirects_markdown(client):
"""Test that a request to /foo redirects to /what-foo-points-at.""" """Test that a request to /foo redirects to /what-foo-points-at."""
response = client.get('/symlink-to-no-title') response = client.get('/symlink-to-no-title')
assert response.status_code == 301 assert response.status_code == 301
assert response.location == '/no-title' assert response.location == 'http://localhost/no-title'
def test_that_request_to_symlink_redirects_file(client): def test_that_request_to_symlink_redirects_file(client):
"""Test that a request to /foo.txt redirects to /what-foo-points-at.txt.""" """Test that a request to /foo.txt redirects to /what-foo-points-at.txt."""
response = client.get('/symlink-to-foo.txt') response = client.get('/symlink-to-foo.txt')
assert response.status_code == 301 assert response.status_code == 301
assert response.location == '/foo.txt' assert response.location == 'http://localhost/foo.txt'
def test_that_request_to_symlink_redirects_directory(client): def test_that_request_to_symlink_redirects_directory(client):
"""Test that a request to /foo/ redirects to /what-foo-points-at/.""" """Test that a request to /foo/ redirects to /what-foo-points-at/."""
response = client.get('/symlink-to-subdir/') response = client.get('/symlink-to-subdir/')
assert response.status_code == 301 assert response.status_code == 301
assert response.location == '/subdir' assert response.location == 'http://localhost/subdir'
# sadly, this location also redirects # sadly, this location also redirects
response = client.get('/subdir') response = client.get('/subdir')
assert response.status_code == 301 assert response.status_code == 301
assert response.location == '/subdir/' assert response.location == 'http://localhost/subdir/'
# but we do get there # but we do get there
response = client.get('/subdir/') response = client.get('/subdir/')
assert response.status_code == 200 assert response.status_code == 200
@ -169,7 +169,7 @@ def test_that_request_to_symlink_redirects_subdirectory(client):
"""Test that a request to /foo/bar redirects to /what-foo-points-at/bar.""" """Test that a request to /foo/bar redirects to /what-foo-points-at/bar."""
response = client.get('/symlink-to-subdir/page-no-title') response = client.get('/symlink-to-subdir/page-no-title')
assert response.status_code == 301 assert response.status_code == 301
assert response.location == '/subdir/page-no-title' assert response.location == 'http://localhost/subdir/page-no-title'
response = client.get('/subdir/page-no-title') response = client.get('/subdir/page-no-title')
assert response.status_code == 200 assert response.status_code == 200

View File

@ -15,10 +15,6 @@ LOGGING = {
}, },
}, },
'loggers': { 'loggers': {
'incorporealcms.mdx': {
'level': 'DEBUG',
'handlers': ['console'],
},
'incorporealcms.pages': { 'incorporealcms.pages': {
'level': 'DEBUG', 'level': 'DEBUG',
'handlers': ['console'], 'handlers': ['console'],

View File

@ -1,13 +0,0 @@
# test of figures
|> ![fancy captioned logo](bss-square-no-bg.png)
|: this is my cool logo!
{: .right }
|> ![vanilla captioned logo](bss-square-no-bg.png)
|: this is my cool logo without an attr!
|> ![fancy logo](bss-square-no-bg.png)
{: .left }
|> ![just a logo](bss-square-no-bg.png)

View File

@ -1,7 +1,7 @@
# test # test
test test
~~~{ pydot:attack-plan } ~~~pydot:attack-plan
digraph G { digraph G {
rankdir=LR rankdir=LR
Earth Earth

View File

@ -1,7 +1,7 @@
# test # test
test test
~~~{ pydot:attack-plan } ~~~pydot:attack-plan
rankdir=LR rankdir=LR
Earth Earth
Mars Mars

View File

@ -1,25 +0,0 @@
# test
test
~~~{ pydot:attack-plan }
digraph G {
rankdir=LR
Earth
Mars
Earth -> Mars
}
~~~
more test
~~~{ pydot:new-attack-plan }
digraph H {
rankdir=LR
Venus
Mars
Venus -> Mars
}
~~~
done