Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
d66a471c76
|
|||
|
bbab9de1f6
|
|||
|
d4f27c9ad8
|
|||
|
424ec3621d
|
|||
|
7205bb2aa5
|
|||
|
88b678931e
|
|||
|
8c75947088
|
|||
|
e75d5c48d2
|
32
CHANGELOG.md
32
CHANGELOG.md
@@ -2,6 +2,38 @@
|
||||
|
||||
Included is a summary of changes to the project, by version. Details can be found in the commit history.
|
||||
|
||||
## v2.0.5
|
||||
|
||||
### Features
|
||||
|
||||
* The Markdown parser replaces links to e.g. `[Page](page.md)` with a href of `page`, rather than the Markdown source
|
||||
specifying a link of `page` explicitly. This allows for some improved site navigation when browsing the Markdown
|
||||
files, e.g. when going to files in Vim, or browsing a site in a Git web UI.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* `tox.ini` also runs tests in a Python 3.13 environment now.
|
||||
* Some trivial bumps to CI requirements.
|
||||
|
||||
## v2.0.4
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* With some significant refactoring, files are now handled better with respect to relative paths, which fixes an issue
|
||||
with symlink pages only properly getting resolved to their target if the symlink was in the `pages/` root rather than
|
||||
a subdir.
|
||||
|
||||
## v2.0.3
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Symlinks for a `.md` file that are to be served by the web server also need a `.html` symlink pointed to the generated
|
||||
file, since the web server is looking for HTML files when serving paths.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
* The project now comes with the GPLv3 "or any later version" clause.
|
||||
|
||||
## v2.0.2
|
||||
|
||||
### Bugfixes
|
||||
|
||||
@@ -24,6 +24,9 @@ logger = logging.getLogger(__name__)
|
||||
def generate_feed(feed_type: str, instance_dir: str, dest_dir: str) -> None:
|
||||
"""Generate the Atom or RSS feed as requested.
|
||||
|
||||
Feed entries should be symlinks to .md files in the pages/ directory, so that they
|
||||
are also linkable and can be browsed outside of the feed.
|
||||
|
||||
Args:
|
||||
feed_type: 'atom' or 'rss' feed
|
||||
instance_dir: the directory for the instance, containing both the feed dir and pages
|
||||
@@ -37,16 +40,18 @@ def generate_feed(feed_type: str, instance_dir: str, dest_dir: str) -> None:
|
||||
fg.link(href=f'https://{Config.DOMAIN_NAME}', rel='alternate')
|
||||
fg.subtitle(f"Blog posts and other interesting materials from {Config.TITLE_SUFFIX}")
|
||||
|
||||
# feed symlinks should all be within the core content subdirectory
|
||||
pages_dir = os.path.join(instance_dir, 'pages')
|
||||
|
||||
# get recent feeds
|
||||
feed_path = os.path.join(instance_dir, 'feed')
|
||||
feed_entry_paths = [os.path.join(dirpath, filename) for dirpath, _, filenames in os.walk(feed_path)
|
||||
for filename in filenames if os.path.islink(os.path.join(dirpath, filename))]
|
||||
for feed_entry_path in sorted(feed_entry_paths):
|
||||
# get the actual file to parse it
|
||||
os.chdir(os.path.abspath(os.path.join(instance_dir, 'pages')))
|
||||
resolved_path = os.path.relpath(os.path.realpath(feed_entry_path), os.path.join(instance_dir, 'pages'))
|
||||
resolved_path = os.path.relpath(os.path.realpath(feed_entry_path), pages_dir)
|
||||
try:
|
||||
content, md, page_name, page_title, mtime = parse_md(resolved_path)
|
||||
content, md, page_name, page_title, mtime = parse_md(os.path.join(pages_dir, resolved_path), pages_dir)
|
||||
link = f'https://{Config.DOMAIN_NAME}{instance_resource_path_to_request_path(resolved_path)}'
|
||||
except (OSError, ValueError, TypeError):
|
||||
logger.exception("error loading/rendering markdown!")
|
||||
|
||||
@@ -44,18 +44,32 @@ def instance_resource_path_to_request_path(path):
|
||||
return '/' + re.sub(r'.md$', '', re.sub(r'index.md$', '', path))
|
||||
|
||||
|
||||
def parse_md(path: str):
|
||||
def parse_md(path: str, pages_root: str):
|
||||
"""Given a file to parse, return file content and other derived data along with the md object.
|
||||
|
||||
Args:
|
||||
path: the path to the file to render
|
||||
pages_root: the absolute path to the pages/ dir, which the path should be within. necessary for
|
||||
proper resolution of resolving parent pages (which needs to know when to stop)
|
||||
"""
|
||||
try:
|
||||
logger.debug("opening path '%s'", path)
|
||||
with open(path, 'r') as input_file:
|
||||
absolute_path = os.path.join(pages_root, path)
|
||||
logger.debug("opening path '%s'", absolute_path)
|
||||
with open(absolute_path, 'r') as input_file:
|
||||
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(input_file.name), tz=datetime.timezone.utc)
|
||||
entry = input_file.read()
|
||||
logger.debug("path '%s' read", path)
|
||||
|
||||
logger.debug("path '%s' read", absolute_path)
|
||||
|
||||
# remove .md extensions used for navigating in vim and replace them with
|
||||
# the pattern we use for HTML output here
|
||||
# foo/index.md -> foo/, foo/index.md#anchor -> foo/#anchor
|
||||
# ../index.md -> ../, ../index.md#anchor -> ../#anchor
|
||||
entry = re.sub(r'\[([^]]+)\]\(([^)]+)index.md(#[^)]*)?\)', r'[\1](\2\3)', entry)
|
||||
# index.md -> ., index.md#anchor -> .#anchor
|
||||
entry = re.sub(r'\[([^]]+)\]\(index.md(#[^)]*)?\)', r'[\1](.\2)', entry)
|
||||
# bar.md -> bar, foo/bar.md -> foo/bar, bar.md#anchor -> bar#anchor
|
||||
entry = re.sub(r'\[([^]]+)\]\(([^)]+).md(#[^)]*)?\)', r'[\1](\2\3)', entry)
|
||||
md = init_md()
|
||||
content = Markup(md.convert(entry)) # nosec B704
|
||||
except (OSError, FileNotFoundError):
|
||||
@@ -67,17 +81,25 @@ def parse_md(path: str):
|
||||
|
||||
logger.debug("file metadata: %s", md.Meta)
|
||||
|
||||
page_name = get_meta_str(md, 'title') if md.Meta.get('title') else instance_resource_path_to_request_path(path)
|
||||
rel_path = os.path.relpath(path, pages_root)
|
||||
page_name = get_meta_str(md, 'title') if md.Meta.get('title') else instance_resource_path_to_request_path(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, mtime
|
||||
|
||||
|
||||
def handle_markdown_file_path(path: str) -> str:
|
||||
"""Given a location on disk, attempt to open it and render the markdown within."""
|
||||
content, md, page_name, page_title, mtime = parse_md(path)
|
||||
parent_navs = generate_parent_navs(path)
|
||||
def handle_markdown_file_path(path: str, pages_root: str) -> str:
|
||||
"""Given a location on disk, attempt to open it and render the markdown within.
|
||||
|
||||
Args:
|
||||
path: the path to the file to parse and produce metadata for
|
||||
pages_root: the absolute path to the pages/ dir, which the path should be within. necessary for
|
||||
proper resolution of resolving parent pages (which needs to know when to stop)
|
||||
"""
|
||||
content, md, page_name, page_title, mtime = parse_md(path, pages_root)
|
||||
relative_path = os.path.relpath(path, pages_root)
|
||||
parent_navs = generate_parent_navs(relative_path, pages_root)
|
||||
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'
|
||||
|
||||
@@ -92,14 +114,21 @@ def handle_markdown_file_path(path: str) -> str:
|
||||
description=get_meta_str(md, 'description'),
|
||||
image=Config.BASE_HOST + get_meta_str(md, 'image'),
|
||||
content=content,
|
||||
base_url=Config.BASE_HOST + instance_resource_path_to_request_path(path),
|
||||
base_url=Config.BASE_HOST + instance_resource_path_to_request_path(relative_path),
|
||||
navs=parent_navs,
|
||||
mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z'),
|
||||
extra_footer=extra_footer)
|
||||
|
||||
|
||||
def generate_parent_navs(path):
|
||||
"""Create a series of paths/links to navigate up from the given resource path."""
|
||||
def generate_parent_navs(path, pages_root: str):
|
||||
"""Create a series of paths/links to navigate up from the given resource path.
|
||||
|
||||
Args:
|
||||
path: the path to parse and generate parent metadata nav links for
|
||||
pages_root: the absolute path to the pages/ dir, which the path should be within. path is relative,
|
||||
but opening parents requires the full path
|
||||
"""
|
||||
logger.debug("path to generate navs for: %s", path)
|
||||
if path == 'index.md':
|
||||
# bail and return the domain name as a terminal case
|
||||
return [(Config.DOMAIN_NAME, '/')]
|
||||
@@ -124,14 +153,14 @@ def generate_parent_navs(path):
|
||||
|
||||
# read the resource
|
||||
try:
|
||||
with open(path, 'r') as entry_file:
|
||||
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))
|
||||
return generate_parent_navs(parent_resource_path) + [(page_name, request_path)]
|
||||
return generate_parent_navs(parent_resource_path, pages_root) + [(page_name, request_path)]
|
||||
except FileNotFoundError:
|
||||
return generate_parent_navs(parent_resource_path) + [(request_path, request_path)]
|
||||
return generate_parent_navs(parent_resource_path, pages_root) + [(request_path, request_path)]
|
||||
|
||||
|
||||
def request_path_to_breadcrumb_display(path):
|
||||
|
||||
@@ -86,7 +86,6 @@ class StaticSiteGenerator(object):
|
||||
convert_markdown: whether or not to convert Markdown files (or simply copy them)
|
||||
"""
|
||||
cprint(f"copying files from '{source_dir}' to '{dest_dir}'", 'green')
|
||||
os.chdir(source_dir)
|
||||
for base_dir, subdirs, files in os.walk(source_dir):
|
||||
logger.debug("starting to build against %s || %s || %s", base_dir, subdirs, files)
|
||||
# remove the absolute path of the directory from the base_dir
|
||||
@@ -113,9 +112,12 @@ class StaticSiteGenerator(object):
|
||||
dest_dir: the output directory to place the subdir in
|
||||
"""
|
||||
dst = os.path.join(dest_dir, base_dir, subdir)
|
||||
if os.path.islink(os.path.join(base_dir, subdir)):
|
||||
absolute_dir = os.path.join(source_dir, base_dir, subdir)
|
||||
logger.debug("checking if %s is a symlink or not", absolute_dir)
|
||||
if os.path.islink(absolute_dir):
|
||||
logger.debug("symlink; raw destination is %s", os.path.realpath(absolute_dir))
|
||||
# keep the link relative to the output directory
|
||||
src = self.symlink_to_relative_dest(source_dir, os.path.join(base_dir, subdir))
|
||||
src = self.symlink_to_relative_dest(source_dir, absolute_dir)
|
||||
print(f"creating directory symlink '{dst}' -> '{src}'")
|
||||
os.symlink(src, dst, target_is_directory=True)
|
||||
else:
|
||||
@@ -137,9 +139,12 @@ class StaticSiteGenerator(object):
|
||||
dest_dir: the output directory to place the subdir in
|
||||
"""
|
||||
dst = os.path.join(dest_dir, base_dir, file_)
|
||||
if os.path.islink(os.path.join(base_dir, file_)):
|
||||
absolute_file = os.path.join(source_dir, base_dir, file_)
|
||||
logger.debug("checking if %s is a symlink or not", absolute_file)
|
||||
if os.path.islink(absolute_file):
|
||||
logger.debug("symlink; raw destination is %s", os.path.realpath(absolute_file))
|
||||
# keep the link relative to the output directory
|
||||
src = self.symlink_to_relative_dest(source_dir, os.path.join(base_dir, file_))
|
||||
src = self.symlink_to_relative_dest(source_dir, absolute_file)
|
||||
print(f"creating symlink '{dst}' -> '{src}'")
|
||||
os.symlink(src, dst, target_is_directory=False)
|
||||
if src.endswith('.md') and convert_markdown:
|
||||
@@ -151,7 +156,7 @@ class StaticSiteGenerator(object):
|
||||
os.symlink(second_src, second_dst, target_is_directory=False)
|
||||
|
||||
else:
|
||||
src = os.path.join(base_dir, file_)
|
||||
src = os.path.join(source_dir, base_dir, file_)
|
||||
print(f"copying file '{src}' -> '{dst}'")
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
@@ -160,7 +165,7 @@ class StaticSiteGenerator(object):
|
||||
rendered_file = dst.removesuffix('.md') + '.html'
|
||||
print(f"rendering file '{src}' -> '{rendered_file}'")
|
||||
try:
|
||||
content = handle_markdown_file_path(src)
|
||||
content = handle_markdown_file_path(src, source_dir)
|
||||
except UnicodeDecodeError:
|
||||
# perhaps this isn't a markdown file at all for some reason; we
|
||||
# copied it above so stick with tha
|
||||
@@ -172,6 +177,10 @@ class StaticSiteGenerator(object):
|
||||
def symlink_to_relative_dest(self, base_dir: str, source: str) -> str:
|
||||
"""Given a symlink, make sure it points to something inside the instance and provide its real destination.
|
||||
|
||||
This is made to be relative to the location of the symlink in all
|
||||
circumstances, in order to avoid breaking out of the instance or output
|
||||
dirs.
|
||||
|
||||
Args:
|
||||
base_dir: the full absolute path of the instance's pages dir, which the symlink destination must be in.
|
||||
source: the symlink to check
|
||||
@@ -180,8 +189,8 @@ class StaticSiteGenerator(object):
|
||||
"""
|
||||
if not os.path.realpath(source).startswith(base_dir):
|
||||
raise ValueError(f"symlink destination {os.path.realpath(source)} is outside the instance!")
|
||||
# this symlink points to realpath inside base_dir, so relative to base_dir, the symlink dest is...
|
||||
return os.path.relpath(os.path.realpath(source), base_dir)
|
||||
# this symlink points to realpath inside base_dir, so relative to the source, the symlink dest is...
|
||||
return os.path.relpath(os.path.realpath(source), os.path.dirname(source))
|
||||
|
||||
|
||||
def build():
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# This file is autogenerated by pip-compile with Python 3.13
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --extra=dev --output-file=requirements/requirements-dev.txt
|
||||
@@ -22,7 +22,7 @@ build==1.2.2.post1
|
||||
# via pip-tools
|
||||
cachetools==5.5.2
|
||||
# via tox
|
||||
certifi==2025.1.31
|
||||
certifi==2025.8.3
|
||||
# via requests
|
||||
cffi==1.17.1
|
||||
# via cryptography
|
||||
@@ -217,9 +217,9 @@ pyyaml==6.0.2
|
||||
# via bandit
|
||||
readme-renderer==44.0
|
||||
# via twine
|
||||
regex==2024.11.6
|
||||
regex==2025.9.1
|
||||
# via nltk
|
||||
requests==2.32.3
|
||||
requests==2.32.5
|
||||
# via
|
||||
# id
|
||||
# requests-toolbelt
|
||||
@@ -240,8 +240,6 @@ ruamel-yaml==0.18.10
|
||||
# via
|
||||
# safety
|
||||
# safety-schemas
|
||||
ruamel-yaml-clib==0.2.12
|
||||
# via ruamel-yaml
|
||||
safety==3.3.1
|
||||
# via incorporeal-cms (pyproject.toml)
|
||||
safety-schemas==0.0.11
|
||||
@@ -278,7 +276,7 @@ typing-extensions==4.12.2
|
||||
# safety
|
||||
# safety-schemas
|
||||
# typer
|
||||
urllib3==2.3.0
|
||||
urllib3==2.5.0
|
||||
# via
|
||||
# requests
|
||||
# twine
|
||||
|
||||
@@ -23,13 +23,11 @@ def test_graphviz_is_rendered():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
src_dir = os.path.join(HERE, 'instance')
|
||||
ssg = StaticSiteGenerator(src_dir, tmpdir)
|
||||
os.chdir(os.path.join(src_dir, 'pages'))
|
||||
|
||||
ssg.build_file_in_destination(os.path.join(HERE, 'instance', 'pages'), '', 'test-graphviz.md', tmpdir, True)
|
||||
with open(os.path.join(tmpdir, 'test-graphviz.html'), 'r') as graphviz_output:
|
||||
data = graphviz_output.read()
|
||||
assert 'data:image/png;base64' in data
|
||||
os.chdir(HERE)
|
||||
|
||||
|
||||
def test_invalid_graphviz_is_not_rendered():
|
||||
@@ -37,12 +35,10 @@ def test_invalid_graphviz_is_not_rendered():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
src_dir = os.path.join(HERE, 'instance')
|
||||
ssg = StaticSiteGenerator(src_dir, tmpdir)
|
||||
os.chdir(os.path.join(src_dir, 'broken'))
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
ssg.build_file_in_destination(os.path.join(HERE, 'instance', 'broken'), '', 'test-invalid-graphviz.md',
|
||||
tmpdir, True)
|
||||
os.chdir(HERE)
|
||||
|
||||
|
||||
def test_figures_are_rendered():
|
||||
@@ -50,7 +46,6 @@ def test_figures_are_rendered():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
src_dir = os.path.join(HERE, 'instance')
|
||||
ssg = StaticSiteGenerator(src_dir, tmpdir)
|
||||
os.chdir(os.path.join(src_dir, 'pages'))
|
||||
|
||||
ssg.build_file_in_destination(os.path.join(HERE, 'instance', 'pages'), '', 'figures.md', tmpdir, True)
|
||||
with open(os.path.join(tmpdir, 'figures.html'), 'r') as graphviz_output:
|
||||
@@ -62,7 +57,6 @@ def test_figures_are_rendered():
|
||||
assert ('<figure class="left"><img alt="fancy logo" src="bss-square-no-bg.png" />'
|
||||
'<span></span></figure>') in data
|
||||
assert '<figure><img alt="just a logo" src="bss-square-no-bg.png" /></figure>' in data
|
||||
os.chdir(HERE)
|
||||
|
||||
|
||||
def test_og_image():
|
||||
@@ -70,7 +64,6 @@ def test_og_image():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
src_dir = os.path.join(HERE, 'instance')
|
||||
ssg = StaticSiteGenerator(src_dir, tmpdir)
|
||||
os.chdir(os.path.join(src_dir, 'pages'))
|
||||
|
||||
ssg.build_file_in_destination(os.path.join(HERE, 'instance', 'pages'), '', 'more-metadata.md', tmpdir, True)
|
||||
with open(os.path.join(tmpdir, 'more-metadata.html'), 'r') as markdown_output:
|
||||
@@ -83,7 +76,6 @@ def test_og_url():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
src_dir = os.path.join(HERE, 'instance')
|
||||
ssg = StaticSiteGenerator(src_dir, tmpdir)
|
||||
os.chdir(os.path.join(src_dir, 'pages'))
|
||||
|
||||
# testing a whole build run because of bugs in how I handle pathing adding a "./" in
|
||||
# the generated URLs for content in the pages/ root
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
"level": "DEBUG",
|
||||
"handlers": ["console"]
|
||||
},
|
||||
"incorporealcms.markdown": {
|
||||
"level": "DEBUG",
|
||||
"handlers": ["console"]
|
||||
},
|
||||
"incorporealcms.ssg": {
|
||||
"level": "DEBUG",
|
||||
"handlers": ["console"]
|
||||
|
||||
6
tests/instance/pages/file-with-index.md-link.md
Normal file
6
tests/instance/pages/file-with-index.md-link.md
Normal file
@@ -0,0 +1,6 @@
|
||||
[Cool](cool/index.md)
|
||||
[Anchored Cool](cool/index.md#anchor)
|
||||
[This Index](index.md)
|
||||
[Anchored This Index](index.md#anchor)
|
||||
[Parent](../index.md)
|
||||
[Anchored Parent](../index.md#anchor)
|
||||
4
tests/instance/pages/file-with-md-link.md
Normal file
4
tests/instance/pages/file-with-md-link.md
Normal file
@@ -0,0 +1,4 @@
|
||||
[Foo](foo.md)
|
||||
[Anchored Foo](foo.md#anchor)
|
||||
[Sub Foo](sub/foo.md)
|
||||
[Anchored Sub Foo](sub/foo.md#anchor)
|
||||
1
tests/instance/pages/subdir/relative-symlink-to-parent.md
Symbolic link
1
tests/instance/pages/subdir/relative-symlink-to-parent.md
Symbolic link
@@ -0,0 +1 @@
|
||||
../more-metadata.md
|
||||
@@ -8,33 +8,38 @@ from unittest.mock import patch
|
||||
|
||||
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)
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
os.chdir(os.path.join(HERE, 'instance/', 'pages/'))
|
||||
INSTANCE_DIR = os.path.join(HERE, 'instance')
|
||||
PAGES_DIR = os.path.join(INSTANCE_DIR, 'pages/')
|
||||
|
||||
# initialize in order to configure debug logging
|
||||
init_instance(INSTANCE_DIR)
|
||||
|
||||
|
||||
def test_generate_page_navs_index():
|
||||
"""Test that the index page has navs to the root (itself)."""
|
||||
assert generate_parent_navs('index.md') == [('example.org', '/')]
|
||||
assert generate_parent_navs('index.md', PAGES_DIR) == [('example.org', '/')]
|
||||
|
||||
|
||||
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') == [('example.org', '/'), ('subdir', '/subdir/')]
|
||||
assert generate_parent_navs('subdir/index.md', PAGES_DIR) == [('example.org', '/'), ('subdir', '/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') == [('example.org', '/'), ('subdir', '/subdir/'),
|
||||
('Page', '/subdir/page')]
|
||||
assert generate_parent_navs('subdir/page.md', PAGES_DIR) == [('example.org', '/'), ('subdir', '/subdir/'),
|
||||
('Page', '/subdir/page')]
|
||||
|
||||
|
||||
def test_generate_page_navs_subdir_with_title_parsing_real_page():
|
||||
"""Test that title metadata is used in the nav text."""
|
||||
assert generate_parent_navs('subdir-with-title/page.md') == [
|
||||
assert generate_parent_navs('subdir-with-title/page.md', PAGES_DIR) == [
|
||||
('example.org', '/'),
|
||||
('SUB!', '/subdir-with-title/'),
|
||||
('page', '/subdir-with-title/page')
|
||||
@@ -43,7 +48,7 @@ def test_generate_page_navs_subdir_with_title_parsing_real_page():
|
||||
|
||||
def test_generate_page_navs_subdir_with_no_index():
|
||||
"""Test that breadcrumbs still generate even if a subdir doesn't have an index.md."""
|
||||
assert generate_parent_navs('no-index-dir/page.md') == [
|
||||
assert generate_parent_navs('no-index-dir/page.md', PAGES_DIR) == [
|
||||
('example.org', '/'),
|
||||
('/no-index-dir/', '/no-index-dir/'),
|
||||
('page', '/no-index-dir/page')
|
||||
@@ -53,26 +58,26 @@ def test_generate_page_navs_subdir_with_no_index():
|
||||
def test_page_includes_themes_with_default():
|
||||
"""Test that a request contains the configured themes and sets the default as appropriate."""
|
||||
assert '<link rel="stylesheet" type="text/css" title="light" href="/static/css/light.css">'\
|
||||
in handle_markdown_file_path('index.md')
|
||||
in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||
assert '<link rel="alternate stylesheet" type="text/css" title="dark" href="/static/css/dark.css">'\
|
||||
in handle_markdown_file_path('index.md')
|
||||
in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||
assert '<a href="" onclick="setStyle(\'light\'); return false;">[light]</a>'\
|
||||
in handle_markdown_file_path('index.md')
|
||||
in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||
assert '<a href="" onclick="setStyle(\'dark\'); return false;">[dark]</a>'\
|
||||
in handle_markdown_file_path('index.md')
|
||||
in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||
|
||||
|
||||
def test_render_with_style_overrides():
|
||||
"""Test that the default can be changed."""
|
||||
with patch('incorporealcms.Config.DEFAULT_PAGE_STYLE', 'dark'):
|
||||
assert '<link rel="stylesheet" type="text/css" title="dark" href="/static/css/dark.css">'\
|
||||
in handle_markdown_file_path('index.md')
|
||||
in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||
assert '<link rel="alternate stylesheet" type="text/css" title="light" href="/static/css/light.css">'\
|
||||
in handle_markdown_file_path('index.md')
|
||||
in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||
assert '<a href="" onclick="setStyle(\'light\'); return false;">[light]</a>'\
|
||||
in handle_markdown_file_path('index.md')
|
||||
in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||
assert '<a href="" onclick="setStyle(\'dark\'); return false;">[dark]</a>'\
|
||||
in handle_markdown_file_path('index.md')
|
||||
in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||
|
||||
|
||||
def test_render_with_default_style_override():
|
||||
@@ -81,23 +86,21 @@ def test_render_with_default_style_override():
|
||||
'warm': '/static/css/warm.css'}):
|
||||
with patch('incorporealcms.Config.DEFAULT_PAGE_STYLE', 'warm'):
|
||||
assert '<link rel="stylesheet" type="text/css" title="warm" href="/static/css/warm.css">'\
|
||||
in handle_markdown_file_path('index.md')
|
||||
in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||
assert '<link rel="alternate stylesheet" type="text/css" title="cool" href="/static/css/cool.css">'\
|
||||
in handle_markdown_file_path('index.md')
|
||||
in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||
assert '<link rel="alternate stylesheet" type="text/css" title="light" href="/static/css/light.css">'\
|
||||
not in handle_markdown_file_path('index.md')
|
||||
not in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||
assert '<a href="" onclick="setStyle(\'warm\'); return false;">[warm]</a>'\
|
||||
in handle_markdown_file_path('index.md')
|
||||
in handle_markdown_file_path('index.md', PAGES_DIR)
|
||||
assert '<a href="" onclick="setStyle(\'cool\'); return false;">[cool]</a>'\
|
||||
in handle_markdown_file_path('index.md')
|
||||
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."""
|
||||
os.chdir(os.path.join(HERE, 'instance/', 'broken/'))
|
||||
with pytest.raises(NotImplementedError):
|
||||
handle_markdown_file_path('redirect.md')
|
||||
os.chdir(os.path.join(HERE, 'instance/', 'pages/'))
|
||||
handle_markdown_file_path('redirect.md', os.path.join(INSTANCE_DIR, 'broken'))
|
||||
|
||||
|
||||
def test_instance_resource_path_to_request_path_on_index():
|
||||
@@ -131,21 +134,21 @@ def test_request_path_to_breadcrumb_display_patterns():
|
||||
|
||||
def test_parse_md_metadata():
|
||||
"""Test the direct results of parsing a markdown file."""
|
||||
content, md, page_name, page_title, mtime = parse_md('more-metadata.md')
|
||||
content, md, page_name, page_title, mtime = parse_md(os.path.join(PAGES_DIR, 'more-metadata.md'), PAGES_DIR)
|
||||
assert page_name == 'title for the page'
|
||||
assert page_title == 'title for the page - example.org'
|
||||
|
||||
|
||||
def test_parse_md_metadata_forced_no_title():
|
||||
"""Test the direct results of parsing a markdown file."""
|
||||
content, md, page_name, page_title, mtime = parse_md('forced-no-title.md')
|
||||
content, md, page_name, page_title, mtime = parse_md(os.path.join(PAGES_DIR, 'forced-no-title.md'), PAGES_DIR)
|
||||
assert page_name == ''
|
||||
assert page_title == 'example.org'
|
||||
|
||||
|
||||
def test_parse_md_metadata_no_title_so_path():
|
||||
"""Test the direct results of parsing a markdown file."""
|
||||
content, md, page_name, page_title, mtime = parse_md('subdir/index.md')
|
||||
content, md, page_name, page_title, mtime = parse_md(os.path.join(PAGES_DIR, 'subdir/index.md'), PAGES_DIR)
|
||||
assert page_name == '/subdir/'
|
||||
assert page_title == '/subdir/ - example.org'
|
||||
|
||||
@@ -153,10 +156,30 @@ def test_parse_md_metadata_no_title_so_path():
|
||||
def test_parse_md_no_file():
|
||||
"""Test the direct results of parsing a markdown file."""
|
||||
with pytest.raises(FileNotFoundError):
|
||||
content, md, page_name, page_title, mtime = parse_md('nope.md')
|
||||
content, md, page_name, page_title, mtime = parse_md(os.path.join(PAGES_DIR, 'nope.md'), PAGES_DIR)
|
||||
|
||||
|
||||
def test_parse_md_bad_file():
|
||||
"""Test the direct results of parsing a markdown file."""
|
||||
with pytest.raises(ValueError):
|
||||
content, md, page_name, page_title, mtime = parse_md('actually-a-png.md')
|
||||
content, md, page_name, page_title, mtime = parse_md(os.path.join(PAGES_DIR, 'actually-a-png.md'), PAGES_DIR)
|
||||
|
||||
|
||||
def test_md_extension_in_source_link_is_stripped():
|
||||
"""Test that if a foo.md file link is specified in the Markdown, it is foo in the HTML."""
|
||||
content, _, _, _, _ = parse_md(os.path.join(PAGES_DIR, 'file-with-md-link.md'), PAGES_DIR)
|
||||
assert '<a href="foo">Foo</a>' in content
|
||||
assert '<a href="foo#anchor">Anchored Foo</a>' in content
|
||||
assert '<a href="sub/foo">Sub Foo</a>' in content
|
||||
assert '<a href="sub/foo#anchor">Anchored Sub Foo</a>' in content
|
||||
|
||||
|
||||
def test_index_in_source_link_is_stripped():
|
||||
"""Test that if a index.md file link is specified in the Markdown, it is just the dir in the HTML."""
|
||||
content, _, _, _, _ = parse_md(os.path.join(PAGES_DIR, 'file-with-index.md-link.md'), PAGES_DIR)
|
||||
assert '<a href="cool/">Cool</a>' in content
|
||||
assert '<a href="cool/#anchor">Anchored Cool</a>' in content
|
||||
assert '<a href=".">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="../#anchor">Anchored Parent</a>' in content
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import incorporealcms.ssg as ssg
|
||||
from incorporealcms import init_instance
|
||||
|
||||
logger = logging.getLogger(__file__)
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
instance_dir = os.path.join(HERE, 'instance')
|
||||
@@ -20,7 +23,6 @@ def test_file_copy():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
src_dir = os.path.join(HERE, 'instance')
|
||||
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
|
||||
os.chdir(os.path.join(instance_dir, 'pages'))
|
||||
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'no-title.md', tmpdir,
|
||||
True)
|
||||
assert os.path.exists(os.path.join(tmpdir, 'no-title.md'))
|
||||
@@ -32,7 +34,6 @@ def test_file_copy_no_markdown():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
src_dir = os.path.join(HERE, 'instance')
|
||||
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
|
||||
os.chdir(os.path.join(instance_dir, 'pages'))
|
||||
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'no-title.md', tmpdir,
|
||||
False)
|
||||
assert os.path.exists(os.path.join(tmpdir, 'no-title.md'))
|
||||
@@ -44,7 +45,6 @@ def test_file_copy_symlink():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
src_dir = os.path.join(HERE, 'instance')
|
||||
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
|
||||
os.chdir(os.path.join(instance_dir, 'pages'))
|
||||
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'symlink-to-foo.txt', tmpdir)
|
||||
# need to copy the destination for os.path.exists to be happy with this
|
||||
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'foo.txt', tmpdir)
|
||||
@@ -52,12 +52,28 @@ def test_file_copy_symlink():
|
||||
assert os.path.islink(os.path.join(tmpdir, 'symlink-to-foo.txt'))
|
||||
|
||||
|
||||
def test_file_copy_subdir_symlink():
|
||||
"""Test the ability to sync a symlink in a subdirectory to the output dir."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
src_dir = os.path.join(HERE, 'instance')
|
||||
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
|
||||
# need to make the subdirectory as if the generator already did
|
||||
os.mkdir(os.path.join(tmpdir, 'subdir'))
|
||||
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), 'subdir',
|
||||
'relative-symlink-to-parent.md', tmpdir)
|
||||
# need to copy the destination for os.path.exists to be happy with this
|
||||
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'more-metadata.md', tmpdir)
|
||||
logger.warning("created symlink %s",
|
||||
os.readlink(os.path.join(tmpdir, 'subdir', 'relative-symlink-to-parent.md')))
|
||||
assert os.path.islink(os.path.join(tmpdir, 'subdir', 'relative-symlink-to-parent.md'))
|
||||
assert os.path.exists(os.path.join(tmpdir, 'subdir', 'relative-symlink-to-parent.md'))
|
||||
|
||||
|
||||
def test_file_copy_symlink_of_markdown_also_has_html_symlink():
|
||||
"""Test the ability to sync source and generated symlinks to the output dir."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
src_dir = os.path.join(HERE, 'instance')
|
||||
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
|
||||
os.chdir(os.path.join(instance_dir, 'pages'))
|
||||
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'symlink-to-no-title.md', tmpdir,
|
||||
True)
|
||||
# need to copy the destination for os.path.exists to be happy with this
|
||||
@@ -74,7 +90,6 @@ def test_dir_copy():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
src_dir = os.path.join(HERE, 'instance')
|
||||
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
|
||||
os.chdir(os.path.join(instance_dir, 'pages'))
|
||||
generator.build_subdir_in_destination(os.path.join(instance_dir, 'pages'), '', 'media', tmpdir)
|
||||
assert os.path.exists(os.path.join(tmpdir, 'media'))
|
||||
assert os.path.isdir(os.path.join(tmpdir, 'media'))
|
||||
@@ -85,7 +100,6 @@ def test_dir_copy_symlink():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
src_dir = os.path.join(HERE, 'instance')
|
||||
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
|
||||
os.chdir(os.path.join(instance_dir, 'pages'))
|
||||
generator.build_subdir_in_destination(os.path.join(instance_dir, 'pages'), '', 'symlink-to-subdir', tmpdir)
|
||||
# need to copy the destination for os.path.exists to be happy with this
|
||||
generator.build_subdir_in_destination(os.path.join(instance_dir, 'pages'), '', 'subdir', tmpdir)
|
||||
|
||||
7
tox.ini
7
tox.ini
@@ -5,7 +5,7 @@
|
||||
|
||||
[tox]
|
||||
isolated_build = true
|
||||
envlist = begin,py39,py310,py311,py312,coverage,security,lint,reuse
|
||||
envlist = begin,py39,py310,py311,py312,py313,coverage,security,lint,reuse
|
||||
|
||||
[testenv]
|
||||
allow_externals = pytest, coverage
|
||||
@@ -41,6 +41,11 @@ commands =
|
||||
commands =
|
||||
pytest --cov-append --cov={envsitepackagesdir}/incorporealcms/ --cov-branch
|
||||
|
||||
[testenv:py313]
|
||||
# run pytest with coverage
|
||||
commands =
|
||||
pytest --cov-append --cov={envsitepackagesdir}/incorporealcms/ --cov-branch
|
||||
|
||||
[testenv:coverage]
|
||||
# report on coverage runs from above
|
||||
skip_install = true
|
||||
|
||||
Reference in New Issue
Block a user