14 Commits

Author SHA1 Message Date
9c937835a9 document v2.1.2
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-02-25 14:48:35 -06:00
f72894f437 add a class for forcing something full width
using this for the occasional figure that I want to be full width but
also have the caption stuff, since the default (that I use often) is
figures are 30%

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-02-25 14:38:20 -06:00
34639edd74 remove the broken/deprecated redirect tag support
when this project *wasn't* a SSG, it could serve up redirects, which is
something that was only rarely needed, and looking now, I don't do it at
all on any of my current sites, so I'm just going to remove the tiny bit
of code for it that remained in the SSG project

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-02-25 09:54:16 -06:00
bcb2b1be7e add the ability to specify the content license in the footer
e.g. for marking all pages as CC BY-SA 4.0

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-02-24 10:08:20 -06:00
d22c3f84ac have floating img/figure clear their side, to avoid "waterfalls"
I have many things floating to the right on the ttrpg site, where the
first would be most right, the second would be right but left of the
first thing, and so on. this forces those to clear their respective side
and create a quasi-column of things rather than making a bizarre ratchet
of content.

.......... A
.......... A
.......... B
(good)

vs.

.......... A
........ B A
(bad)

I can't see why I'd want the bad behavior on my other sites, but I'll
just mention that I didn't check to see what they were doing

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-02-24 10:02:57 -06:00
2d5528fa82 style super links for footnotes
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-02-24 10:00:31 -06:00
23c4c57f2f give the plain HTML table some borders
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-02-24 10:00:30 -06:00
6a7d009f35 style hr in the main light/dark styles, used in footnotes
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-02-24 10:00:28 -06:00
7ec8c05bb4 slightly tweak footnote refs, and actually style footnotes
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-02-24 10:00:26 -06:00
b10fe555df tweak table header bg color to stand out less
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-02-24 10:00:24 -06:00
20e8cdbbf1 remove unnecessary type="text/javascript"
CSS validator says: Warning: The type attribute is unnecessary for JavaScript resources

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-02-24 10:00:15 -06:00
e056f57797 Changelog for v2.1.1
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-01-28 16:16:11 -06:00
9b7ab74644 remove unused request_path_to_breadcrumb_display
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-01-28 16:10:53 -06:00
204e7bc416 use h1-as-title logic while generating breadcrumbs
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-01-28 16:08:41 -06:00
9 changed files with 114 additions and 52 deletions

View File

@@ -2,6 +2,37 @@
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
### Features

View File

@@ -84,6 +84,21 @@ def parse_md(path: str, pages_root: str):
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')
# 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'):
page_name = h1_tag.string
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
page_description = None
@@ -103,10 +118,7 @@ def parse_md(path: str, pages_root: str):
if page_description := p_tag.string:
page_description = page_description.replace('\n', ' ')
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
return page_name, page_description
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
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)
return template.render(title=page_title,
config=Config,
@@ -175,16 +182,8 @@ def generate_parent_navs(path, pages_root: str):
try:
with open(os.path.join(pages_root, path), 'r') as entry_file:
entry = entry_file.read()
_ = Markup(md.convert(entry)) # nosec B704
page_name = (" ".join(md.Meta.get('title')) if md.Meta.get('title')
else request_path_to_breadcrumb_display(request_path))
content = Markup(md.convert(entry)) # nosec B704
page_name, _ = _get_metadata_from_parsed_page(md, content, os.path.relpath(path, parent_resource_dir))
return generate_parent_navs(parent_resource_path, pages_root) + [(page_name, request_path)]
except FileNotFoundError:
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('/')

View File

@@ -116,19 +116,26 @@ img {
max-width: 75% !important;
}
.full-width {
max-width: 100%;
}
.img-center {
display: block;
clear: both;
margin-left: auto;
margin-right: auto;
}
.img-left {
float: left;
clear: left;
margin-right: 1em;
}
.img-right {
float: right;
clear: right;
margin-left: 1em;
}
@@ -144,12 +151,14 @@ figure {
figure.right {
float: right;
clear: right;
margin-left: 10px;
display: block;
}
figure.left {
float: left;
clear: left;
margin-right: 10px;
display: block;
}
@@ -164,14 +173,19 @@ figcaption {
font-size: 0.9em;
}
.footnote {
div.content .footnote {
font-size: 0.8em;
}
.footnote p {
div.content .footnote p {
margin: 0;
}
.footnote-ref:link, .footnote-ref:visited, .footnote-ref:hover, .footnote-ref:active {
font-weight: normal;
}
.footnote-ref {
font-size: 0.75em;
margin-left: 1px;
}

View File

@@ -14,11 +14,15 @@ body {
background: #111;
}
hr {
color: #333;
}
h1, h2, h3, h4, h5, h6 {
color: #B31D15;
}
p a, ul a, ol a {
p a, ul a, ol a, sup a {
color: #DDD;
}
@@ -26,7 +30,7 @@ footer a {
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;
}
@@ -44,7 +48,7 @@ table, th, td {
}
th {
background: #333;
background: #111;
}
blockquote {

View File

@@ -14,11 +14,15 @@ body {
background: #EEE;
}
hr {
color: #CCC;
}
h1, h2, h3, h4, h5, h6 {
color: #811610;
}
p a, ul a, ol a {
p a, ul a, ol a, sup a {
color: #222;
}
@@ -26,7 +30,7 @@ footer a {
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;
}
@@ -44,7 +48,7 @@ table, th, td {
}
th {
background: #CCC;
background: #EEE;
}
blockquote {

View File

@@ -9,6 +9,10 @@ div.header {
justify-content: space-between;
}
table, th, td {
border: 1px solid;
}
.img-25 {
max-width: 25% !important;
}

View File

@@ -18,7 +18,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<link rel="icon" href="{{ config.FAVICON }}">
<link rel="alternate" type="application/atom+xml" href="/feed/atom">
<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 %}>
{% block header %}
@@ -44,7 +44,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
<footer>
{% 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>
{% endblock %}
</div>

View File

@@ -1 +0,0 @@
Redirect: http://www.google.com/

View File

@@ -10,8 +10,7 @@ import pytest
from incorporealcms import init_instance
from incorporealcms.markdown import (generate_parent_navs, handle_markdown_file_path,
instance_resource_path_to_request_path, parse_md,
request_path_to_breadcrumb_display)
instance_resource_path_to_request_path, parse_md)
HERE = os.path.dirname(os.path.abspath(__file__))
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', '/')]
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():
"""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():
"""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')]
@@ -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) == [
('example.org', '/'),
('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) == [
('example.org', '/'),
('/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)
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():
"""Test 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'
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():
"""Test the direct results of parsing a markdown file."""
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="../">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)