Compare commits

..

10 Commits

Author SHA1 Message Date
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
ee4215ede2
Changelog for v2.1.0
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-01-28 14:46:54 -06:00
f7e211564e
refer to py3.10 in instructions, now that 3.9 is unsupported
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-01-28 14:40:45 -06:00
8238787900
use beautifulsoup to parse the description from the first paragraph
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-01-28 14:40:45 -06:00
20673c178a
use beautifulsoup to derive title from HTML h1
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-01-28 14:40:45 -06:00
3ca13cc6f8
requirements bump
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-01-28 14:40:45 -06:00
7b2bf6905a
test the high level SSG build command
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-01-28 14:40:45 -06:00
dd2f5eeaea
remove python 3.9 from supported versions
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2026-01-28 14:40:45 -06:00
15 changed files with 288 additions and 144 deletions

View File

@ -2,6 +2,35 @@
Included is a summary of changes to the project, by version. Details can be found in the commit history.
## 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
* The page title (also used in the `og:title` header) and the optional description used in the `og:description` header
can be derived from the contents of the page content, if the markdown meta tags are not supplied. The first `h1` is
used for the title, and the first `p` is used for the description. This is largely to save some time writing pages
that one wants to look nice, especially in a social media card, and removes some repetition.
### Miscellaneous
* Requirements bumped, which led to...
* Python 3.9 has been removed from the supported versions.
* Added some miscellaneous unit tests and coverage changes to keep us at 95% (which only dropped for a library reason I
don't understand).
## v2.0.5
### Features

View File

@ -7,8 +7,8 @@ A lightweight static site generator for Markdown-based sites.
Something like the following should suffice:
```
% virtualenv --python=python3.9 env-py3.9
% source env-py3.9/bin/activate
% virtualenv --python=python3.10 env-py3.10
% source env-py3.10/bin/activate
% pip install -U pip
% pip install incorporeal-cms
% incorporealcms-build ./path/to/instance ./path/to/output/www/root

View File

@ -22,7 +22,7 @@ jinja_env = Environment(
try:
# packaged/pip install -e . value
from ._version import version as __version__
except ImportError:
except ImportError: # pragma: no cover
# local clone value
from setuptools_scm import get_version
__version__ = get_version(root='..', relative_to=__file__)

View File

@ -51,7 +51,7 @@ def generate_feed(feed_type: str, instance_dir: str, dest_dir: str) -> None:
# get the actual file to parse it
resolved_path = os.path.relpath(os.path.realpath(feed_entry_path), pages_dir)
try:
content, md, page_name, page_title, mtime = parse_md(os.path.join(pages_dir, resolved_path), pages_dir)
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!")

View File

@ -13,6 +13,7 @@ import os
import re
import markdown
from bs4 import BeautifulSoup
from markupsafe import Markup
from incorporealcms import jinja_env
@ -82,11 +83,42 @@ def parse_md(path: str, pages_root: str):
logger.debug("file metadata: %s", md.Meta)
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_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, mtime
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
page_name = None
if md.Meta.get('title'):
page_name = get_meta_str(md, 'title')
elif h1_tag := soup.find('h1'):
page_name = h1_tag.string
elif not page_name:
page_name = instance_resource_path_to_request_path(path)
# get the page description from the markdown tags or first paragraph
page_description = None
if md.Meta.get('description'):
page_description = get_meta_str(md, 'description')
elif p_tag := soup.find('p'):
if page_description := p_tag.string:
page_description = page_description.replace('\n', ' ')
return page_name, page_description
def handle_markdown_file_path(path: str, pages_root: str) -> str:
@ -97,7 +129,7 @@ def handle_markdown_file_path(path: str, pages_root: str) -> str:
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)
content, md, page_name, page_title, page_description, 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
@ -111,7 +143,7 @@ def handle_markdown_file_path(path: str, pages_root: str) -> str:
template = jinja_env.get_template(template_name)
return template.render(title=page_title,
config=Config,
description=get_meta_str(md, 'description'),
description=page_description,
image=Config.BASE_HOST + get_meta_str(md, 'image'),
content=content,
base_url=Config.BASE_HOST + instance_resource_path_to_request_path(relative_path),
@ -155,16 +187,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

@ -10,8 +10,8 @@ license = {text = "GPL-3.0-or-later"}
authors = [
{name = "Brian S. Stephan", email = "bss@incorporeal.org"},
]
requires-python = ">=3.9"
dependencies = ["feedgen", "jinja2", "Markdown", "termcolor"]
requires-python = ">=3.10"
dependencies = ["beautifulsoup4", "feedgen", "jinja2", "Markdown", "termcolor"]
dynamic = ["version"]
classifiers = [
"Programming Language :: Python :: 3",

View File

@ -6,35 +6,36 @@
#
annotated-types==0.7.0
# via pydantic
attrs==25.3.0
anyio==4.12.1
# via httpx
attrs==25.4.0
# via reuse
authlib==1.5.1
authlib==1.6.6
# via safety
bandit==1.8.3
bandit==1.9.3
# via incorporeal-cms (pyproject.toml)
binaryornot==0.4.4
# via reuse
boolean-py==4.0
# via
# license-expression
# reuse
build==1.2.2.post1
beautifulsoup4==4.14.3
# via incorporeal-cms (pyproject.toml)
boolean-py==5.0
# via license-expression
build==1.4.0
# via pip-tools
cachetools==5.5.2
cachetools==6.2.6
# via tox
certifi==2025.8.3
# via requests
cffi==1.17.1
certifi==2026.1.4
# via
# httpcore
# httpx
# requests
cffi==2.0.0
# via cryptography
chardet==5.2.0
# via
# binaryornot
# tox
charset-normalizer==3.4.1
# via tox
charset-normalizer==3.4.4
# via
# python-debian
# requests
click==8.1.8
click==8.3.1
# via
# nltk
# pip-tools
@ -43,17 +44,17 @@ click==8.1.8
# typer
colorama==0.4.6
# via tox
coverage[toml]==7.7.0
coverage[toml]==7.13.2
# via pytest-cov
cryptography==44.0.2
cryptography==46.0.4
# via
# authlib
# secretstorage
distlib==0.3.9
distlib==0.4.0
# via virtualenv
dlint==0.16.0
# via incorporeal-cms (pyproject.toml)
docutils==0.21.2
docutils==0.22.4
# via readme-renderer
dparse==0.6.4
# via
@ -61,12 +62,12 @@ dparse==0.6.4
# safety-schemas
feedgen==1.0.0
# via incorporeal-cms (pyproject.toml)
filelock==3.16.1
filelock==3.20.3
# via
# safety
# tox
# virtualenv
flake8==7.1.2
flake8==7.3.0
# via
# dlint
# flake8-builtins
@ -78,7 +79,7 @@ flake8==7.1.2
# incorporeal-cms (pyproject.toml)
flake8-blind-except==0.2.1
# via incorporeal-cms (pyproject.toml)
flake8-builtins==2.5.0
flake8-builtins==3.1.0
# via incorporeal-cms (pyproject.toml)
flake8-docstrings==1.7.0
# via incorporeal-cms (pyproject.toml)
@ -86,27 +87,36 @@ flake8-executable==2.1.3
# via incorporeal-cms (pyproject.toml)
flake8-fixme==1.1.1
# via incorporeal-cms (pyproject.toml)
flake8-isort==6.1.2
flake8-isort==7.0.0
# via incorporeal-cms (pyproject.toml)
flake8-logging-format==2024.24.12
# via incorporeal-cms (pyproject.toml)
flake8-mutable==1.2.0
# via incorporeal-cms (pyproject.toml)
flake8-pyproject==1.2.3
flake8-pyproject==1.2.4
# via incorporeal-cms (pyproject.toml)
h11==0.16.0
# via httpcore
httpcore==1.0.9
# via httpx
httpx==0.28.1
# via safety
id==1.5.0
# via twine
idna==3.10
# via requests
iniconfig==2.0.0
idna==3.11
# via
# anyio
# httpx
# requests
iniconfig==2.3.0
# via pytest
isort==6.0.1
isort==7.0.0
# via flake8-isort
jaraco-classes==3.4.0
# via keyring
jaraco-context==6.0.1
jaraco-context==6.1.0
# via keyring
jaraco-functools==4.1.0
jaraco-functools==4.4.0
# via keyring
jeepney==0.9.0
# via
@ -117,43 +127,44 @@ jinja2==3.1.6
# incorporeal-cms (pyproject.toml)
# reuse
# safety
joblib==1.4.2
joblib==1.5.3
# via nltk
keyring==25.6.0
keyring==25.7.0
# via twine
license-expression==30.4.1
librt==0.7.8
# via mypy
license-expression==30.4.4
# via reuse
lxml==5.3.1
lxml==6.0.2
# via feedgen
markdown==3.7
markdown==3.10.1
# via incorporeal-cms (pyproject.toml)
markdown-it-py==3.0.0
markdown-it-py==4.0.0
# via rich
markupsafe==3.0.2
markupsafe==3.0.3
# via jinja2
marshmallow==3.26.1
marshmallow==4.2.1
# via safety
mccabe==0.7.0
# via flake8
mdurl==0.1.2
# via markdown-it-py
more-itertools==10.6.0
more-itertools==10.8.0
# via
# jaraco-classes
# jaraco-functools
mypy==1.15.0
mypy==1.19.1
# via incorporeal-cms (pyproject.toml)
mypy-extensions==1.0.0
mypy-extensions==1.1.0
# via mypy
nh3==0.2.21
nh3==0.3.2
# via readme-renderer
nltk==3.9.1
nltk==3.9.2
# via safety
packaging==24.2
packaging==26.0
# via
# build
# dparse
# marshmallow
# pyproject-api
# pytest
# safety
@ -161,63 +172,66 @@ packaging==24.2
# setuptools-scm
# tox
# twine
pbr==6.1.1
# via stevedore
pip-tools==7.4.1
# wheel
pathspec==1.0.4
# via mypy
pip-tools==7.5.2
# via incorporeal-cms (pyproject.toml)
platformdirs==4.3.6
platformdirs==4.5.1
# via
# tox
# virtualenv
pluggy==1.5.0
pluggy==1.6.0
# via
# pytest
# pytest-cov
# tox
psutil==6.1.1
# via safety
pycodestyle==2.12.1
pycodestyle==2.14.0
# via flake8
pycparser==2.22
pycparser==3.0
# via cffi
pydantic==2.9.2
pydantic==2.12.5
# via
# safety
# safety-schemas
pydantic-core==2.23.4
pydantic-core==2.41.5
# via pydantic
pydocstyle==6.3.0
# via flake8-docstrings
pydot==3.0.4
pydot==4.0.1
# via incorporeal-cms (pyproject.toml)
pyflakes==3.2.0
pyflakes==3.4.0
# via flake8
pygments==2.19.1
pygments==2.19.2
# via
# pytest
# readme-renderer
# rich
pyparsing==3.2.1
pyparsing==3.3.2
# via pydot
pyproject-api==1.9.0
pyproject-api==1.10.0
# via tox
pyproject-hooks==1.2.0
# via
# build
# pip-tools
pytest==8.3.5
pytest==9.0.2
# via
# incorporeal-cms (pyproject.toml)
# pytest-cov
pytest-cov==6.0.0
pytest-cov==7.0.0
# via incorporeal-cms (pyproject.toml)
python-dateutil==2.9.0.post0
# via feedgen
python-debian==1.0.1
# via reuse
pyyaml==6.0.2
python-magic==0.4.27
# via reuse
pyyaml==6.0.3
# via bandit
readme-renderer==44.0
# via twine
regex==2025.9.1
regex==2026.1.15
# via nltk
requests==2.32.5
# via
@ -227,62 +241,72 @@ requests==2.32.5
# twine
requests-toolbelt==1.0.0
# via twine
reuse==5.0.2
reuse==6.2.0
# via incorporeal-cms (pyproject.toml)
rfc3986==2.0.0
# via twine
rich==13.9.4
rich==14.3.1
# via
# bandit
# twine
# typer
ruamel-yaml==0.18.10
ruamel-yaml==0.19.1
# via
# safety
# safety-schemas
safety==3.3.1
safety==3.7.0
# via incorporeal-cms (pyproject.toml)
safety-schemas==0.0.11
safety-schemas==0.0.16
# via safety
secretstorage==3.3.3
secretstorage==3.5.0
# via keyring
setuptools-scm==8.2.0
setuptools-scm==9.2.2
# via incorporeal-cms (pyproject.toml)
shellingham==1.5.4
# via typer
six==1.17.0
# via python-dateutil
snowballstemmer==2.2.0
snowballstemmer==3.0.1
# via pydocstyle
stevedore==5.4.1
soupsieve==2.8.3
# via beautifulsoup4
stevedore==5.6.0
# via bandit
termcolor==2.5.0
tenacity==9.1.2
# via safety
termcolor==3.3.0
# via incorporeal-cms (pyproject.toml)
tomlkit==0.13.2
# via reuse
tox==4.24.2
tomlkit==0.14.0
# via
# reuse
# safety
tox==4.34.1
# via incorporeal-cms (pyproject.toml)
tqdm==4.67.1
# via nltk
twine==6.1.0
twine==6.2.0
# via incorporeal-cms (pyproject.toml)
typer==0.15.2
typer==0.21.1
# via safety
typing-extensions==4.12.2
typing-extensions==4.15.0
# via
# beautifulsoup4
# mypy
# pydantic
# pydantic-core
# safety
# safety-schemas
# typer
urllib3==2.5.0
# typing-inspection
typing-inspection==0.4.2
# via pydantic
urllib3==2.6.3
# via
# requests
# twine
virtualenv==20.29.3
virtualenv==20.36.1
# via tox
wheel==0.45.1
wheel==0.46.3
# via pip-tools
# The following packages are considered to be unsafe in a requirements file:

View File

@ -1,22 +1,28 @@
#
# 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 --output-file=requirements/requirements.txt
#
beautifulsoup4==4.14.3
# via incorporeal-cms (pyproject.toml)
feedgen==1.0.0
# via incorporeal-cms (pyproject.toml)
jinja2==3.1.6
# via incorporeal-cms (pyproject.toml)
lxml==5.3.1
lxml==6.0.2
# via feedgen
markdown==3.7
markdown==3.10.1
# via incorporeal-cms (pyproject.toml)
markupsafe==3.0.2
markupsafe==3.0.3
# via jinja2
python-dateutil==2.9.0.post0
# via feedgen
six==1.17.0
# via python-dateutil
termcolor==2.5.0
soupsieve==2.8.3
# via beautifulsoup4
termcolor==3.3.0
# via incorporeal-cms (pyproject.toml)
typing-extensions==4.15.0
# via beautifulsoup4

View File

@ -0,0 +1 @@
there's just some words here but no title tag or h1

View File

@ -0,0 +1 @@
there's just some words here but no title tag or h1

View File

@ -0,0 +1 @@
there's just some words here but no title tag or h1

View File

@ -0,0 +1,6 @@
# rambling test for inferred description
this is a long string of text where
I am typing a lot over multiple lines
this second paragraph shouldn't be in the metadata

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')
]
@ -123,51 +129,79 @@ 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, mtime = parse_md(os.path.join(PAGES_DIR, 'more-metadata.md'), PAGES_DIR)
content, md, page_name, page_title, page_desc, 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'
assert page_desc == 'description of this page made even longer'
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(os.path.join(PAGES_DIR, 'forced-no-title.md'), PAGES_DIR)
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():
def test_parse_md_metadata_no_title_so_h1():
"""Test the direct results of parsing a markdown file."""
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'
content, md, page_name, page_title, _, mtime = parse_md(os.path.join(PAGES_DIR, 'subdir/index.md'), PAGES_DIR)
assert page_name == 'another page'
assert page_title == 'another page - example.org'
def test_parse_md_metadata_no_title_or_h1_so_path():
"""Test the direct results of parsing a markdown file."""
content, md, page_name, page_title, _, mtime = parse_md(os.path.join(PAGES_DIR, 'no-title-or-h1.md'), PAGES_DIR)
assert page_name == '/no-title-or-h1'
assert page_title == '/no-title-or-h1 - example.org'
def test_parse_md_metadata_no_title_or_h1_so_path_dir():
"""Test the direct results of parsing a markdown file."""
content, md, page_name, page_title, _, mtime = parse_md(os.path.join(PAGES_DIR, 'no-title-subdir/index.md'),
PAGES_DIR)
assert page_name == '/no-title-subdir/'
assert page_title == '/no-title-subdir/ - example.org'
def test_parse_md_metadata_no_title_or_h1_so_path_dir_file():
"""Test the direct results of parsing a markdown file."""
content, md, page_name, page_title, _, mtime = parse_md(os.path.join(PAGES_DIR,
'no-title-subdir/no-title-or-h1.md'),
PAGES_DIR)
assert page_name == '/no-title-subdir/no-title-or-h1'
assert page_title == '/no-title-subdir/no-title-or-h1 - example.org'
def test_parse_md_derive_description_from_p():
"""Test that we can get a description from the first paragraph in the file."""
content, md, page_name, page_title, page_desc, mtime = parse_md(
os.path.join(PAGES_DIR, 'rambling.md'),
PAGES_DIR
)
assert page_desc == 'this is a long string of text where I am typing a lot over multiple lines'
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(os.path.join(PAGES_DIR, 'nope.md'), PAGES_DIR)
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(os.path.join(PAGES_DIR, 'actually-a-png.md'), PAGES_DIR)
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)
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
@ -176,7 +210,7 @@ def test_md_extension_in_source_link_is_stripped():
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)
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

View File

@ -129,3 +129,26 @@ def test_build_in_destination_ignores_dot_files():
generator.build_in_destination(os.path.join(src_dir, 'pages'), tmpdir)
assert not os.path.exists(os.path.join(tmpdir, '.ignored-file.md'))
def test_build():
"""Test that the high level build can work against two directories."""
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
generator.build()
assert os.path.exists(os.path.join(tmpdir, 'index.md'))
assert os.path.exists(os.path.join(tmpdir, 'index.html'))
assert os.path.exists(os.path.join(tmpdir, 'subdir', 'index.md'))
assert os.path.exists(os.path.join(tmpdir, 'subdir', 'index.html'))
assert os.path.exists(os.path.join(tmpdir, 'symlink-to-subdir'))
assert os.path.isdir(os.path.join(tmpdir, 'symlink-to-subdir'))
assert os.path.islink(os.path.join(tmpdir, 'symlink-to-subdir'))
assert os.path.exists(os.path.join(tmpdir, 'media'))
assert os.path.isdir(os.path.join(tmpdir, 'media'))
assert not os.path.exists(os.path.join(tmpdir, '.ignored-file.md'))
assert os.path.exists(os.path.join(tmpdir, 'feed'))
assert os.path.isdir(os.path.join(tmpdir, 'feed'))
assert os.path.exists(os.path.join(tmpdir, 'feed/atom'))
assert os.path.exists(os.path.join(tmpdir, 'feed/rss'))

View File

@ -5,7 +5,7 @@
[tox]
isolated_build = true
envlist = begin,py39,py310,py311,py312,py313,coverage,security,lint,reuse
envlist = begin,py310,py311,py312,py313,coverage,security,lint,reuse
[testenv]
allow_externals = pytest, coverage
@ -21,11 +21,6 @@ deps = setuptools
skip_install = true
commands = coverage erase
[testenv:py39]
# run pytest with coverage
commands =
pytest --cov-append --cov={envsitepackagesdir}/incorporealcms/ --cov-branch
[testenv:py310]
# run pytest with coverage
commands =