Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
9c937835a9
|
|||
|
f72894f437
|
|||
|
34639edd74
|
|||
|
bcb2b1be7e
|
|||
|
d22c3f84ac
|
|||
|
2d5528fa82
|
|||
|
23c4c57f2f
|
|||
|
6a7d009f35
|
|||
|
7ec8c05bb4
|
|||
|
b10fe555df
|
|||
|
20e8cdbbf1
|
|||
|
e056f57797
|
|||
|
9b7ab74644
|
|||
|
204e7bc416
|
31
CHANGELOG.md
31
CHANGELOG.md
@@ -2,6 +2,37 @@
|
|||||||
|
|
||||||
Included is a summary of changes to the project, by version. Details can be found in the commit history.
|
Included is a summary of changes to the project, by version. Details can be found in the commit history.
|
||||||
|
|
||||||
|
## v2.1.2
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* An optional license declaration can be added to the footer, with a config "LICENSE" directive.
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
* Style changes in footnotes, hrs, table colors, footnote links, full width figures.
|
||||||
|
* Have floats clear their side, to not have a waterfall/ratchet effect when too many floating things are next to each
|
||||||
|
other.
|
||||||
|
* Add borders to the plain style tables.
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
* One HTML tweak to make the W3C CSS validator happy.
|
||||||
|
* Some old code from the pre-SSG days has been removed.
|
||||||
|
|
||||||
|
## v2.1.1
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
* Use the h1-as-name feature from v2.1.0 also to generate the page name in breadcrumbs. This changes the behavior on
|
||||||
|
pages with an h1 but no Title: meta tag to have a better name, of course, but also changes the behavior on pages with
|
||||||
|
neither a h1 nor a Title: meta tag to have a leading slash (e.g. /page-filename) where there previously was not one
|
||||||
|
(e.g. just page-filename). This seems like an acceptable trade-off.
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
* With the minor breadcrumb change, a method used to finagle the breadcrumb no-name name is no longer necessary.
|
||||||
|
|
||||||
## v2.1.0
|
## v2.1.0
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -84,6 +84,21 @@ def parse_md(path: str, pages_root: str):
|
|||||||
|
|
||||||
rel_path = os.path.relpath(path, pages_root)
|
rel_path = os.path.relpath(path, pages_root)
|
||||||
|
|
||||||
|
page_name, page_description = _get_metadata_from_parsed_page(md, content, rel_path)
|
||||||
|
page_title = f'{page_name} - {Config.TITLE_SUFFIX}' if page_name else Config.TITLE_SUFFIX
|
||||||
|
logger.debug("title (potentially derived): %s", page_title)
|
||||||
|
|
||||||
|
return content, md, page_name, page_title, page_description, mtime
|
||||||
|
|
||||||
|
|
||||||
|
def _get_metadata_from_parsed_page(md, content, path: str):
|
||||||
|
"""Get the page name and description from a Markdown object and/or HTML output of a page.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
md: the parsed Markdown object, potentially including Meta tags
|
||||||
|
content: the Markdown page content converted to HTML, to run through BeautifulSoup
|
||||||
|
path: path of the page, to derive the name from as a fallback
|
||||||
|
"""
|
||||||
soup = BeautifulSoup(content, features='lxml')
|
soup = BeautifulSoup(content, features='lxml')
|
||||||
|
|
||||||
# get the page title first from the markdown tags, second from the first h1, last from the path
|
# get the page title first from the markdown tags, second from the first h1, last from the path
|
||||||
@@ -93,7 +108,7 @@ def parse_md(path: str, pages_root: str):
|
|||||||
elif h1_tag := soup.find('h1'):
|
elif h1_tag := soup.find('h1'):
|
||||||
page_name = h1_tag.string
|
page_name = h1_tag.string
|
||||||
elif not page_name:
|
elif not page_name:
|
||||||
page_name = instance_resource_path_to_request_path(rel_path)
|
page_name = instance_resource_path_to_request_path(path)
|
||||||
|
|
||||||
# get the page description from the markdown tags or first paragraph
|
# get the page description from the markdown tags or first paragraph
|
||||||
page_description = None
|
page_description = None
|
||||||
@@ -103,10 +118,7 @@ def parse_md(path: str, pages_root: str):
|
|||||||
if page_description := p_tag.string:
|
if page_description := p_tag.string:
|
||||||
page_description = page_description.replace('\n', ' ')
|
page_description = page_description.replace('\n', ' ')
|
||||||
|
|
||||||
page_title = f'{page_name} - {Config.TITLE_SUFFIX}' if page_name else Config.TITLE_SUFFIX
|
return page_name, page_description
|
||||||
logger.debug("title (potentially derived): %s", page_title)
|
|
||||||
|
|
||||||
return content, md, page_name, page_title, page_description, mtime
|
|
||||||
|
|
||||||
|
|
||||||
def handle_markdown_file_path(path: str, pages_root: str) -> str:
|
def handle_markdown_file_path(path: str, pages_root: str) -> str:
|
||||||
@@ -123,11 +135,6 @@ def handle_markdown_file_path(path: str, pages_root: str) -> str:
|
|||||||
extra_footer = get_meta_str(md, 'footer') if md.Meta.get('footer') else None
|
extra_footer = get_meta_str(md, 'footer') if md.Meta.get('footer') else None
|
||||||
template_name = get_meta_str(md, 'template') if md.Meta.get('template') else 'base.html'
|
template_name = get_meta_str(md, 'template') if md.Meta.get('template') else 'base.html'
|
||||||
|
|
||||||
# check if this has a HTTP redirect
|
|
||||||
redirect_url = get_meta_str(md, 'redirect') if md.Meta.get('redirect') else None
|
|
||||||
if redirect_url:
|
|
||||||
raise NotImplementedError("redirects in markdown are unsupported!")
|
|
||||||
|
|
||||||
template = jinja_env.get_template(template_name)
|
template = jinja_env.get_template(template_name)
|
||||||
return template.render(title=page_title,
|
return template.render(title=page_title,
|
||||||
config=Config,
|
config=Config,
|
||||||
@@ -175,16 +182,8 @@ def generate_parent_navs(path, pages_root: str):
|
|||||||
try:
|
try:
|
||||||
with open(os.path.join(pages_root, path), 'r') as entry_file:
|
with open(os.path.join(pages_root, path), 'r') as entry_file:
|
||||||
entry = entry_file.read()
|
entry = entry_file.read()
|
||||||
_ = Markup(md.convert(entry)) # nosec B704
|
content = Markup(md.convert(entry)) # nosec B704
|
||||||
page_name = (" ".join(md.Meta.get('title')) if md.Meta.get('title')
|
page_name, _ = _get_metadata_from_parsed_page(md, content, os.path.relpath(path, parent_resource_dir))
|
||||||
else request_path_to_breadcrumb_display(request_path))
|
|
||||||
return generate_parent_navs(parent_resource_path, pages_root) + [(page_name, request_path)]
|
return generate_parent_navs(parent_resource_path, pages_root) + [(page_name, request_path)]
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return generate_parent_navs(parent_resource_path, pages_root) + [(request_path, request_path)]
|
return generate_parent_navs(parent_resource_path, pages_root) + [(request_path, request_path)]
|
||||||
|
|
||||||
|
|
||||||
def request_path_to_breadcrumb_display(path):
|
|
||||||
"""Given a request path, e.g. "/foo/bar/baz/", turn it into breadcrumby text "baz"."""
|
|
||||||
undired = path.rstrip('/')
|
|
||||||
leaf = undired[undired.rfind('/'):]
|
|
||||||
return leaf.strip('/')
|
|
||||||
|
|||||||
@@ -116,19 +116,26 @@ img {
|
|||||||
max-width: 75% !important;
|
max-width: 75% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.img-center {
|
.img-center {
|
||||||
display: block;
|
display: block;
|
||||||
|
clear: both;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.img-left {
|
.img-left {
|
||||||
float: left;
|
float: left;
|
||||||
|
clear: left;
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.img-right {
|
.img-right {
|
||||||
float: right;
|
float: right;
|
||||||
|
clear: right;
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,12 +151,14 @@ figure {
|
|||||||
|
|
||||||
figure.right {
|
figure.right {
|
||||||
float: right;
|
float: right;
|
||||||
|
clear: right;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
figure.left {
|
figure.left {
|
||||||
float: left;
|
float: left;
|
||||||
|
clear: left;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -164,14 +173,19 @@ figcaption {
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footnote {
|
div.content .footnote {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footnote p {
|
div.content .footnote p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footnote-ref:link, .footnote-ref:visited, .footnote-ref:hover, .footnote-ref:active {
|
.footnote-ref:link, .footnote-ref:visited, .footnote-ref:hover, .footnote-ref:active {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footnote-ref {
|
||||||
|
font-size: 0.75em;
|
||||||
|
margin-left: 1px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,11 +14,15 @@ body {
|
|||||||
background: #111;
|
background: #111;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
color: #B31D15;
|
color: #B31D15;
|
||||||
}
|
}
|
||||||
|
|
||||||
p a, ul a, ol a {
|
p a, ul a, ol a, sup a {
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +30,7 @@ footer a {
|
|||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
p a:hover, ul a:hover, ol a:hover, footer a:hover {
|
p a:hover, ul a:hover, ol a:hover, footer a:hover, sup a:hover {
|
||||||
color: #B31D15;
|
color: #B31D15;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +48,7 @@ table, th, td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background: #333;
|
background: #111;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
|
|||||||
@@ -14,11 +14,15 @@ body {
|
|||||||
background: #EEE;
|
background: #EEE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
color: #CCC;
|
||||||
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
color: #811610;
|
color: #811610;
|
||||||
}
|
}
|
||||||
|
|
||||||
p a, ul a, ol a {
|
p a, ul a, ol a, sup a {
|
||||||
color: #222;
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +30,7 @@ footer a {
|
|||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
p a:hover, ul a:hover, ol a:hover, footer a:hover {
|
p a:hover, ul a:hover, ol a:hover, footer a:hover, sup a:hover {
|
||||||
color: #811610;
|
color: #811610;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +48,7 @@ table, th, td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background: #CCC;
|
background: #EEE;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ div.header {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table, th, td {
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
.img-25 {
|
.img-25 {
|
||||||
max-width: 25% !important;
|
max-width: 25% !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<link rel="icon" href="{{ config.FAVICON }}">
|
<link rel="icon" href="{{ config.FAVICON }}">
|
||||||
<link rel="alternate" type="application/atom+xml" href="/feed/atom">
|
<link rel="alternate" type="application/atom+xml" href="/feed/atom">
|
||||||
<link rel="alternate" type="application/rss+xml" href="/feed/rss">
|
<link rel="alternate" type="application/rss+xml" href="/feed/rss">
|
||||||
<script type="text/javascript" src="/static/js/style_switcher.js"></script>
|
<script src="/static/js/style_switcher.js"></script>
|
||||||
|
|
||||||
<div {% block site_class %}class="site-wrap site-wrap-normal-width"{% endblock %}>
|
<div {% block site_class %}class="site-wrap site-wrap-normal-width"{% endblock %}>
|
||||||
{% block header %}
|
{% block header %}
|
||||||
@@ -44,7 +44,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
{% if extra_footer %}<div class="extra-footer"><i>{{ extra_footer|safe }}</i></div>{% endif %}
|
{% if extra_footer %}<div class="extra-footer"><i>{{ extra_footer|safe }}</i></div>{% endif %}
|
||||||
<div class="footer"><i>Last modified: {{ mtime }}</i></div>
|
<div class="footer">
|
||||||
|
<i>Last modified: {{ mtime }}.<br />
|
||||||
|
{% if config.LICENSE %} Available via {{ config.LICENSE|safe }}{% endif %}.
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Redirect: http://www.google.com/
|
|
||||||
@@ -10,8 +10,7 @@ import pytest
|
|||||||
|
|
||||||
from incorporealcms import init_instance
|
from incorporealcms import init_instance
|
||||||
from incorporealcms.markdown import (generate_parent_navs, handle_markdown_file_path,
|
from incorporealcms.markdown import (generate_parent_navs, handle_markdown_file_path,
|
||||||
instance_resource_path_to_request_path, parse_md,
|
instance_resource_path_to_request_path, parse_md)
|
||||||
request_path_to_breadcrumb_display)
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
INSTANCE_DIR = os.path.join(HERE, 'instance')
|
INSTANCE_DIR = os.path.join(HERE, 'instance')
|
||||||
@@ -26,14 +25,21 @@ def test_generate_page_navs_index():
|
|||||||
assert generate_parent_navs('index.md', PAGES_DIR) == [('example.org', '/')]
|
assert generate_parent_navs('index.md', PAGES_DIR) == [('example.org', '/')]
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_page_navs_title_from_h1():
|
||||||
|
"""Test that the index page has navs to the root (itself)."""
|
||||||
|
assert generate_parent_navs('no-title.md', PAGES_DIR) == [('example.org', '/'),
|
||||||
|
('this page doesn\'t have a title!', '/no-title')]
|
||||||
|
|
||||||
|
|
||||||
def test_generate_page_navs_subdir_index():
|
def test_generate_page_navs_subdir_index():
|
||||||
"""Test that dir pages have navs to the root and themselves."""
|
"""Test that dir pages have navs to the root and themselves."""
|
||||||
assert generate_parent_navs('subdir/index.md', PAGES_DIR) == [('example.org', '/'), ('subdir', '/subdir/')]
|
assert generate_parent_navs('subdir/index.md', PAGES_DIR) == [('example.org', '/'), ('another page', '/subdir/')]
|
||||||
|
|
||||||
|
|
||||||
def test_generate_page_navs_subdir_real_page():
|
def test_generate_page_navs_subdir_real_page():
|
||||||
"""Test that real pages have navs to the root, their parent, and themselves."""
|
"""Test that real pages have navs to the root, their parent, and themselves."""
|
||||||
assert generate_parent_navs('subdir/page.md', PAGES_DIR) == [('example.org', '/'), ('subdir', '/subdir/'),
|
assert generate_parent_navs('subdir/page.md', PAGES_DIR) == [('example.org', '/'),
|
||||||
|
('another page', '/subdir/'),
|
||||||
('Page', '/subdir/page')]
|
('Page', '/subdir/page')]
|
||||||
|
|
||||||
|
|
||||||
@@ -42,7 +48,7 @@ def test_generate_page_navs_subdir_with_title_parsing_real_page():
|
|||||||
assert generate_parent_navs('subdir-with-title/page.md', PAGES_DIR) == [
|
assert generate_parent_navs('subdir-with-title/page.md', PAGES_DIR) == [
|
||||||
('example.org', '/'),
|
('example.org', '/'),
|
||||||
('SUB!', '/subdir-with-title/'),
|
('SUB!', '/subdir-with-title/'),
|
||||||
('page', '/subdir-with-title/page')
|
('/page', '/subdir-with-title/page')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -51,7 +57,7 @@ def test_generate_page_navs_subdir_with_no_index():
|
|||||||
assert generate_parent_navs('no-index-dir/page.md', PAGES_DIR) == [
|
assert generate_parent_navs('no-index-dir/page.md', PAGES_DIR) == [
|
||||||
('example.org', '/'),
|
('example.org', '/'),
|
||||||
('/no-index-dir/', '/no-index-dir/'),
|
('/no-index-dir/', '/no-index-dir/'),
|
||||||
('page', '/no-index-dir/page')
|
('/page', '/no-index-dir/page')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -97,12 +103,6 @@ def test_render_with_default_style_override():
|
|||||||
in handle_markdown_file_path('index.md', PAGES_DIR)
|
in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||||
|
|
||||||
|
|
||||||
def test_redirects_error_unsupported():
|
|
||||||
"""Test that we throw a warning about the barely-used Markdown redirect tag, which we can't support via SSG."""
|
|
||||||
with pytest.raises(NotImplementedError):
|
|
||||||
handle_markdown_file_path('redirect.md', os.path.join(INSTANCE_DIR, 'broken'))
|
|
||||||
|
|
||||||
|
|
||||||
def test_instance_resource_path_to_request_path_on_index():
|
def test_instance_resource_path_to_request_path_on_index():
|
||||||
"""Test index.md -> /."""
|
"""Test index.md -> /."""
|
||||||
assert instance_resource_path_to_request_path('index.md') == '/'
|
assert instance_resource_path_to_request_path('index.md') == '/'
|
||||||
@@ -123,15 +123,6 @@ def test_instance_resource_path_to_request_path_on_subdir_and_page():
|
|||||||
assert instance_resource_path_to_request_path('subdir/page.md') == '/subdir/page'
|
assert instance_resource_path_to_request_path('subdir/page.md') == '/subdir/page'
|
||||||
|
|
||||||
|
|
||||||
def test_request_path_to_breadcrumb_display_patterns():
|
|
||||||
"""Test various conversions from request path to leaf nodes for display in the breadcrumbs."""
|
|
||||||
assert request_path_to_breadcrumb_display('/foo') == 'foo'
|
|
||||||
assert request_path_to_breadcrumb_display('/foo/') == 'foo'
|
|
||||||
assert request_path_to_breadcrumb_display('/foo/bar') == 'bar'
|
|
||||||
assert request_path_to_breadcrumb_display('/foo/bar/') == 'bar'
|
|
||||||
assert request_path_to_breadcrumb_display('/') == ''
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_md_metadata():
|
def test_parse_md_metadata():
|
||||||
"""Test the direct results of parsing a markdown file."""
|
"""Test the direct results of parsing a markdown file."""
|
||||||
content, md, page_name, page_title, page_desc, mtime = parse_md(
|
content, md, page_name, page_title, page_desc, mtime = parse_md(
|
||||||
@@ -220,3 +211,15 @@ def test_index_in_source_link_is_stripped():
|
|||||||
assert '<a href=".#anchor">Anchored This Index</a>' in content
|
assert '<a href=".#anchor">Anchored This Index</a>' in content
|
||||||
assert '<a href="../">Parent</a>' in content
|
assert '<a href="../">Parent</a>' in content
|
||||||
assert '<a href="../#anchor">Anchored Parent</a>' in content
|
assert '<a href="../#anchor">Anchored Parent</a>' in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_license_link():
|
||||||
|
"""Test that the config's license HTML is displayed in the footer."""
|
||||||
|
with patch('incorporealcms.Config.LICENSE',
|
||||||
|
'<a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>', create=True):
|
||||||
|
assert 'Available via <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>.'\
|
||||||
|
in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||||
|
|
||||||
|
# default, no config
|
||||||
|
assert '<a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>'\
|
||||||
|
not in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||||
|
|||||||
Reference in New Issue
Block a user