Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
9c937835a9
|
|||
|
f72894f437
|
|||
|
34639edd74
|
|||
|
bcb2b1be7e
|
|||
|
d22c3f84ac
|
|||
|
2d5528fa82
|
|||
|
23c4c57f2f
|
|||
|
6a7d009f35
|
|||
|
7ec8c05bb4
|
|||
|
b10fe555df
|
|||
|
20e8cdbbf1
|
|||
|
e056f57797
|
|||
|
9b7ab74644
|
|||
|
204e7bc416
|
|||
|
ee4215ede2
|
|||
|
f7e211564e
|
|||
|
8238787900
|
|||
|
20673c178a
|
|||
|
3ca13cc6f8
|
|||
|
7b2bf6905a
|
|||
|
dd2f5eeaea
|
|||
|
d66a471c76
|
|||
|
bbab9de1f6
|
|||
|
d4f27c9ad8
|
|||
|
424ec3621d
|
|||
|
7205bb2aa5
|
|||
|
88b678931e
|
|||
|
8c75947088
|
|||
|
e75d5c48d2
|
79
CHANGELOG.md
79
CHANGELOG.md
@@ -2,6 +2,85 @@
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
* 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
|
## v2.0.2
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ A lightweight static site generator for Markdown-based sites.
|
|||||||
Something like the following should suffice:
|
Something like the following should suffice:
|
||||||
|
|
||||||
```
|
```
|
||||||
% virtualenv --python=python3.9 env-py3.9
|
% virtualenv --python=python3.10 env-py3.10
|
||||||
% source env-py3.9/bin/activate
|
% source env-py3.10/bin/activate
|
||||||
% pip install -U pip
|
% pip install -U pip
|
||||||
% pip install incorporeal-cms
|
% pip install incorporeal-cms
|
||||||
% incorporealcms-build ./path/to/instance ./path/to/output/www/root
|
% incorporealcms-build ./path/to/instance ./path/to/output/www/root
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jinja_env = Environment(
|
|||||||
try:
|
try:
|
||||||
# packaged/pip install -e . value
|
# packaged/pip install -e . value
|
||||||
from ._version import version as __version__
|
from ._version import version as __version__
|
||||||
except ImportError:
|
except ImportError: # pragma: no cover
|
||||||
# local clone value
|
# local clone value
|
||||||
from setuptools_scm import get_version
|
from setuptools_scm import get_version
|
||||||
__version__ = get_version(root='..', relative_to=__file__)
|
__version__ = get_version(root='..', relative_to=__file__)
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ logger = logging.getLogger(__name__)
|
|||||||
def generate_feed(feed_type: str, instance_dir: str, dest_dir: str) -> None:
|
def generate_feed(feed_type: str, instance_dir: str, dest_dir: str) -> None:
|
||||||
"""Generate the Atom or RSS feed as requested.
|
"""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:
|
Args:
|
||||||
feed_type: 'atom' or 'rss' feed
|
feed_type: 'atom' or 'rss' feed
|
||||||
instance_dir: the directory for the instance, containing both the feed dir and pages
|
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.link(href=f'https://{Config.DOMAIN_NAME}', rel='alternate')
|
||||||
fg.subtitle(f"Blog posts and other interesting materials from {Config.TITLE_SUFFIX}")
|
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
|
# get recent feeds
|
||||||
feed_path = os.path.join(instance_dir, 'feed')
|
feed_path = os.path.join(instance_dir, 'feed')
|
||||||
feed_entry_paths = [os.path.join(dirpath, filename) for dirpath, _, filenames in os.walk(feed_path)
|
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 filename in filenames if os.path.islink(os.path.join(dirpath, filename))]
|
||||||
for feed_entry_path in sorted(feed_entry_paths):
|
for feed_entry_path in sorted(feed_entry_paths):
|
||||||
# get the actual file to parse it
|
# 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), pages_dir)
|
||||||
resolved_path = os.path.relpath(os.path.realpath(feed_entry_path), os.path.join(instance_dir, 'pages'))
|
|
||||||
try:
|
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)}'
|
link = f'https://{Config.DOMAIN_NAME}{instance_resource_path_to_request_path(resolved_path)}'
|
||||||
except (OSError, ValueError, TypeError):
|
except (OSError, ValueError, TypeError):
|
||||||
logger.exception("error loading/rendering markdown!")
|
logger.exception("error loading/rendering markdown!")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
import markdown
|
import markdown
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
|
|
||||||
from incorporealcms import jinja_env
|
from incorporealcms import jinja_env
|
||||||
@@ -44,18 +45,32 @@ def instance_resource_path_to_request_path(path):
|
|||||||
return '/' + re.sub(r'.md$', '', re.sub(r'index.md$', '', 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.
|
"""Given a file to parse, return file content and other derived data along with the md object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: the path to the file to render
|
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:
|
try:
|
||||||
logger.debug("opening path '%s'", path)
|
absolute_path = os.path.join(pages_root, path)
|
||||||
with open(path, 'r') as input_file:
|
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)
|
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(input_file.name), tz=datetime.timezone.utc)
|
||||||
entry = input_file.read()
|
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()
|
md = init_md()
|
||||||
content = Markup(md.convert(entry)) # nosec B704
|
content = Markup(md.convert(entry)) # nosec B704
|
||||||
except (OSError, FileNotFoundError):
|
except (OSError, FileNotFoundError):
|
||||||
@@ -67,39 +82,80 @@ def parse_md(path: str):
|
|||||||
|
|
||||||
logger.debug("file metadata: %s", md.Meta)
|
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, 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
|
page_title = f'{page_name} - {Config.TITLE_SUFFIX}' if page_name else Config.TITLE_SUFFIX
|
||||||
logger.debug("title (potentially derived): %s", page_title)
|
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 handle_markdown_file_path(path: str) -> str:
|
def _get_metadata_from_parsed_page(md, content, path: str):
|
||||||
"""Given a location on disk, attempt to open it and render the markdown within."""
|
"""Get the page name and description from a Markdown object and/or HTML output of a page.
|
||||||
content, md, page_name, page_title, mtime = parse_md(path)
|
|
||||||
parent_navs = generate_parent_navs(path)
|
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:
|
||||||
|
"""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, 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
|
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,
|
||||||
description=get_meta_str(md, 'description'),
|
description=page_description,
|
||||||
image=Config.BASE_HOST + get_meta_str(md, 'image'),
|
image=Config.BASE_HOST + get_meta_str(md, 'image'),
|
||||||
content=content,
|
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,
|
navs=parent_navs,
|
||||||
mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z'),
|
mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z'),
|
||||||
extra_footer=extra_footer)
|
extra_footer=extra_footer)
|
||||||
|
|
||||||
|
|
||||||
def generate_parent_navs(path):
|
def generate_parent_navs(path, pages_root: str):
|
||||||
"""Create a series of paths/links to navigate up from the given resource path."""
|
"""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':
|
if path == 'index.md':
|
||||||
# bail and return the domain name as a terminal case
|
# bail and return the domain name as a terminal case
|
||||||
return [(Config.DOMAIN_NAME, '/')]
|
return [(Config.DOMAIN_NAME, '/')]
|
||||||
@@ -124,18 +180,10 @@ def generate_parent_navs(path):
|
|||||||
|
|
||||||
# read the resource
|
# read the resource
|
||||||
try:
|
try:
|
||||||
with open(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) + [(page_name, request_path)]
|
|
||||||
except FileNotFoundError:
|
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):
|
|
||||||
"""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('/')
|
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ class StaticSiteGenerator(object):
|
|||||||
convert_markdown: whether or not to convert Markdown files (or simply copy them)
|
convert_markdown: whether or not to convert Markdown files (or simply copy them)
|
||||||
"""
|
"""
|
||||||
cprint(f"copying files from '{source_dir}' to '{dest_dir}'", 'green')
|
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):
|
for base_dir, subdirs, files in os.walk(source_dir):
|
||||||
logger.debug("starting to build against %s || %s || %s", base_dir, subdirs, files)
|
logger.debug("starting to build against %s || %s || %s", base_dir, subdirs, files)
|
||||||
# remove the absolute path of the directory from the base_dir
|
# 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
|
dest_dir: the output directory to place the subdir in
|
||||||
"""
|
"""
|
||||||
dst = os.path.join(dest_dir, base_dir, subdir)
|
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
|
# 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}'")
|
print(f"creating directory symlink '{dst}' -> '{src}'")
|
||||||
os.symlink(src, dst, target_is_directory=True)
|
os.symlink(src, dst, target_is_directory=True)
|
||||||
else:
|
else:
|
||||||
@@ -137,9 +139,12 @@ class StaticSiteGenerator(object):
|
|||||||
dest_dir: the output directory to place the subdir in
|
dest_dir: the output directory to place the subdir in
|
||||||
"""
|
"""
|
||||||
dst = os.path.join(dest_dir, base_dir, file_)
|
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
|
# 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}'")
|
print(f"creating symlink '{dst}' -> '{src}'")
|
||||||
os.symlink(src, dst, target_is_directory=False)
|
os.symlink(src, dst, target_is_directory=False)
|
||||||
if src.endswith('.md') and convert_markdown:
|
if src.endswith('.md') and convert_markdown:
|
||||||
@@ -151,7 +156,7 @@ class StaticSiteGenerator(object):
|
|||||||
os.symlink(second_src, second_dst, target_is_directory=False)
|
os.symlink(second_src, second_dst, target_is_directory=False)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
src = os.path.join(base_dir, file_)
|
src = os.path.join(source_dir, base_dir, file_)
|
||||||
print(f"copying file '{src}' -> '{dst}'")
|
print(f"copying file '{src}' -> '{dst}'")
|
||||||
shutil.copy2(src, dst)
|
shutil.copy2(src, dst)
|
||||||
|
|
||||||
@@ -160,7 +165,7 @@ class StaticSiteGenerator(object):
|
|||||||
rendered_file = dst.removesuffix('.md') + '.html'
|
rendered_file = dst.removesuffix('.md') + '.html'
|
||||||
print(f"rendering file '{src}' -> '{rendered_file}'")
|
print(f"rendering file '{src}' -> '{rendered_file}'")
|
||||||
try:
|
try:
|
||||||
content = handle_markdown_file_path(src)
|
content = handle_markdown_file_path(src, source_dir)
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
# perhaps this isn't a markdown file at all for some reason; we
|
# perhaps this isn't a markdown file at all for some reason; we
|
||||||
# copied it above so stick with tha
|
# 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:
|
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.
|
"""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:
|
Args:
|
||||||
base_dir: the full absolute path of the instance's pages dir, which the symlink destination must be in.
|
base_dir: the full absolute path of the instance's pages dir, which the symlink destination must be in.
|
||||||
source: the symlink to check
|
source: the symlink to check
|
||||||
@@ -180,8 +189,8 @@ class StaticSiteGenerator(object):
|
|||||||
"""
|
"""
|
||||||
if not os.path.realpath(source).startswith(base_dir):
|
if not os.path.realpath(source).startswith(base_dir):
|
||||||
raise ValueError(f"symlink destination {os.path.realpath(source)} is outside the instance!")
|
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...
|
# 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), base_dir)
|
return os.path.relpath(os.path.realpath(source), os.path.dirname(source))
|
||||||
|
|
||||||
|
|
||||||
def build():
|
def build():
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ license = {text = "GPL-3.0-or-later"}
|
|||||||
authors = [
|
authors = [
|
||||||
{name = "Brian S. Stephan", email = "bss@incorporeal.org"},
|
{name = "Brian S. Stephan", email = "bss@incorporeal.org"},
|
||||||
]
|
]
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.10"
|
||||||
dependencies = ["feedgen", "jinja2", "Markdown", "termcolor"]
|
dependencies = ["beautifulsoup4", "feedgen", "jinja2", "Markdown", "termcolor"]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
|
|||||||
@@ -1,40 +1,41 @@
|
|||||||
#
|
#
|
||||||
# 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:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile --extra=dev --output-file=requirements/requirements-dev.txt
|
# pip-compile --extra=dev --output-file=requirements/requirements-dev.txt
|
||||||
#
|
#
|
||||||
annotated-types==0.7.0
|
annotated-types==0.7.0
|
||||||
# via pydantic
|
# via pydantic
|
||||||
attrs==25.3.0
|
anyio==4.12.1
|
||||||
|
# via httpx
|
||||||
|
attrs==25.4.0
|
||||||
# via reuse
|
# via reuse
|
||||||
authlib==1.5.1
|
authlib==1.6.6
|
||||||
# via safety
|
# via safety
|
||||||
bandit==1.8.3
|
bandit==1.9.3
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
binaryornot==0.4.4
|
beautifulsoup4==4.14.3
|
||||||
# via reuse
|
# via incorporeal-cms (pyproject.toml)
|
||||||
boolean-py==4.0
|
boolean-py==5.0
|
||||||
# via
|
# via license-expression
|
||||||
# license-expression
|
build==1.4.0
|
||||||
# reuse
|
|
||||||
build==1.2.2.post1
|
|
||||||
# via pip-tools
|
# via pip-tools
|
||||||
cachetools==5.5.2
|
cachetools==6.2.6
|
||||||
# via tox
|
# via tox
|
||||||
certifi==2025.1.31
|
certifi==2026.1.4
|
||||||
# via requests
|
# via
|
||||||
cffi==1.17.1
|
# httpcore
|
||||||
|
# httpx
|
||||||
|
# requests
|
||||||
|
cffi==2.0.0
|
||||||
# via cryptography
|
# via cryptography
|
||||||
chardet==5.2.0
|
chardet==5.2.0
|
||||||
# via
|
# via tox
|
||||||
# binaryornot
|
charset-normalizer==3.4.4
|
||||||
# tox
|
|
||||||
charset-normalizer==3.4.1
|
|
||||||
# via
|
# via
|
||||||
# python-debian
|
# python-debian
|
||||||
# requests
|
# requests
|
||||||
click==8.1.8
|
click==8.3.1
|
||||||
# via
|
# via
|
||||||
# nltk
|
# nltk
|
||||||
# pip-tools
|
# pip-tools
|
||||||
@@ -43,17 +44,17 @@ click==8.1.8
|
|||||||
# typer
|
# typer
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
# via tox
|
# via tox
|
||||||
coverage[toml]==7.7.0
|
coverage[toml]==7.13.2
|
||||||
# via pytest-cov
|
# via pytest-cov
|
||||||
cryptography==44.0.2
|
cryptography==46.0.4
|
||||||
# via
|
# via
|
||||||
# authlib
|
# authlib
|
||||||
# secretstorage
|
# secretstorage
|
||||||
distlib==0.3.9
|
distlib==0.4.0
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
dlint==0.16.0
|
dlint==0.16.0
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
docutils==0.21.2
|
docutils==0.22.4
|
||||||
# via readme-renderer
|
# via readme-renderer
|
||||||
dparse==0.6.4
|
dparse==0.6.4
|
||||||
# via
|
# via
|
||||||
@@ -61,12 +62,12 @@ dparse==0.6.4
|
|||||||
# safety-schemas
|
# safety-schemas
|
||||||
feedgen==1.0.0
|
feedgen==1.0.0
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
filelock==3.16.1
|
filelock==3.20.3
|
||||||
# via
|
# via
|
||||||
# safety
|
# safety
|
||||||
# tox
|
# tox
|
||||||
# virtualenv
|
# virtualenv
|
||||||
flake8==7.1.2
|
flake8==7.3.0
|
||||||
# via
|
# via
|
||||||
# dlint
|
# dlint
|
||||||
# flake8-builtins
|
# flake8-builtins
|
||||||
@@ -78,7 +79,7 @@ flake8==7.1.2
|
|||||||
# incorporeal-cms (pyproject.toml)
|
# incorporeal-cms (pyproject.toml)
|
||||||
flake8-blind-except==0.2.1
|
flake8-blind-except==0.2.1
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
flake8-builtins==2.5.0
|
flake8-builtins==3.1.0
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
flake8-docstrings==1.7.0
|
flake8-docstrings==1.7.0
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
@@ -86,27 +87,36 @@ flake8-executable==2.1.3
|
|||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
flake8-fixme==1.1.1
|
flake8-fixme==1.1.1
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
flake8-isort==6.1.2
|
flake8-isort==7.0.0
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
flake8-logging-format==2024.24.12
|
flake8-logging-format==2024.24.12
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
flake8-mutable==1.2.0
|
flake8-mutable==1.2.0
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
flake8-pyproject==1.2.3
|
flake8-pyproject==1.2.4
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# 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
|
id==1.5.0
|
||||||
# via twine
|
# via twine
|
||||||
idna==3.10
|
idna==3.11
|
||||||
# via requests
|
# via
|
||||||
iniconfig==2.0.0
|
# anyio
|
||||||
|
# httpx
|
||||||
|
# requests
|
||||||
|
iniconfig==2.3.0
|
||||||
# via pytest
|
# via pytest
|
||||||
isort==6.0.1
|
isort==7.0.0
|
||||||
# via flake8-isort
|
# via flake8-isort
|
||||||
jaraco-classes==3.4.0
|
jaraco-classes==3.4.0
|
||||||
# via keyring
|
# via keyring
|
||||||
jaraco-context==6.0.1
|
jaraco-context==6.1.0
|
||||||
# via keyring
|
# via keyring
|
||||||
jaraco-functools==4.1.0
|
jaraco-functools==4.4.0
|
||||||
# via keyring
|
# via keyring
|
||||||
jeepney==0.9.0
|
jeepney==0.9.0
|
||||||
# via
|
# via
|
||||||
@@ -117,43 +127,44 @@ jinja2==3.1.6
|
|||||||
# incorporeal-cms (pyproject.toml)
|
# incorporeal-cms (pyproject.toml)
|
||||||
# reuse
|
# reuse
|
||||||
# safety
|
# safety
|
||||||
joblib==1.4.2
|
joblib==1.5.3
|
||||||
# via nltk
|
# via nltk
|
||||||
keyring==25.6.0
|
keyring==25.7.0
|
||||||
# via twine
|
# via twine
|
||||||
license-expression==30.4.1
|
librt==0.7.8
|
||||||
|
# via mypy
|
||||||
|
license-expression==30.4.4
|
||||||
# via reuse
|
# via reuse
|
||||||
lxml==5.3.1
|
lxml==6.0.2
|
||||||
# via feedgen
|
# via feedgen
|
||||||
markdown==3.7
|
markdown==3.10.1
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
markdown-it-py==3.0.0
|
markdown-it-py==4.0.0
|
||||||
# via rich
|
# via rich
|
||||||
markupsafe==3.0.2
|
markupsafe==3.0.3
|
||||||
# via jinja2
|
# via jinja2
|
||||||
marshmallow==3.26.1
|
marshmallow==4.2.1
|
||||||
# via safety
|
# via safety
|
||||||
mccabe==0.7.0
|
mccabe==0.7.0
|
||||||
# via flake8
|
# via flake8
|
||||||
mdurl==0.1.2
|
mdurl==0.1.2
|
||||||
# via markdown-it-py
|
# via markdown-it-py
|
||||||
more-itertools==10.6.0
|
more-itertools==10.8.0
|
||||||
# via
|
# via
|
||||||
# jaraco-classes
|
# jaraco-classes
|
||||||
# jaraco-functools
|
# jaraco-functools
|
||||||
mypy==1.15.0
|
mypy==1.19.1
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
mypy-extensions==1.0.0
|
mypy-extensions==1.1.0
|
||||||
# via mypy
|
# via mypy
|
||||||
nh3==0.2.21
|
nh3==0.3.2
|
||||||
# via readme-renderer
|
# via readme-renderer
|
||||||
nltk==3.9.1
|
nltk==3.9.2
|
||||||
# via safety
|
# via safety
|
||||||
packaging==24.2
|
packaging==26.0
|
||||||
# via
|
# via
|
||||||
# build
|
# build
|
||||||
# dparse
|
# dparse
|
||||||
# marshmallow
|
|
||||||
# pyproject-api
|
# pyproject-api
|
||||||
# pytest
|
# pytest
|
||||||
# safety
|
# safety
|
||||||
@@ -161,65 +172,68 @@ packaging==24.2
|
|||||||
# setuptools-scm
|
# setuptools-scm
|
||||||
# tox
|
# tox
|
||||||
# twine
|
# twine
|
||||||
pbr==6.1.1
|
# wheel
|
||||||
# via stevedore
|
pathspec==1.0.4
|
||||||
pip-tools==7.4.1
|
# via mypy
|
||||||
|
pip-tools==7.5.2
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
platformdirs==4.3.6
|
platformdirs==4.5.1
|
||||||
# via
|
# via
|
||||||
# tox
|
# tox
|
||||||
# virtualenv
|
# virtualenv
|
||||||
pluggy==1.5.0
|
pluggy==1.6.0
|
||||||
# via
|
# via
|
||||||
# pytest
|
# pytest
|
||||||
|
# pytest-cov
|
||||||
# tox
|
# tox
|
||||||
psutil==6.1.1
|
pycodestyle==2.14.0
|
||||||
# via safety
|
|
||||||
pycodestyle==2.12.1
|
|
||||||
# via flake8
|
# via flake8
|
||||||
pycparser==2.22
|
pycparser==3.0
|
||||||
# via cffi
|
# via cffi
|
||||||
pydantic==2.9.2
|
pydantic==2.12.5
|
||||||
# via
|
# via
|
||||||
# safety
|
# safety
|
||||||
# safety-schemas
|
# safety-schemas
|
||||||
pydantic-core==2.23.4
|
pydantic-core==2.41.5
|
||||||
# via pydantic
|
# via pydantic
|
||||||
pydocstyle==6.3.0
|
pydocstyle==6.3.0
|
||||||
# via flake8-docstrings
|
# via flake8-docstrings
|
||||||
pydot==3.0.4
|
pydot==4.0.1
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
pyflakes==3.2.0
|
pyflakes==3.4.0
|
||||||
# via flake8
|
# via flake8
|
||||||
pygments==2.19.1
|
pygments==2.19.2
|
||||||
# via
|
# via
|
||||||
|
# pytest
|
||||||
# readme-renderer
|
# readme-renderer
|
||||||
# rich
|
# rich
|
||||||
pyparsing==3.2.1
|
pyparsing==3.3.2
|
||||||
# via pydot
|
# via pydot
|
||||||
pyproject-api==1.9.0
|
pyproject-api==1.10.0
|
||||||
# via tox
|
# via tox
|
||||||
pyproject-hooks==1.2.0
|
pyproject-hooks==1.2.0
|
||||||
# via
|
# via
|
||||||
# build
|
# build
|
||||||
# pip-tools
|
# pip-tools
|
||||||
pytest==8.3.5
|
pytest==9.0.2
|
||||||
# via
|
# via
|
||||||
# incorporeal-cms (pyproject.toml)
|
# incorporeal-cms (pyproject.toml)
|
||||||
# pytest-cov
|
# pytest-cov
|
||||||
pytest-cov==6.0.0
|
pytest-cov==7.0.0
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
# via feedgen
|
# via feedgen
|
||||||
python-debian==1.0.1
|
python-debian==1.0.1
|
||||||
# via reuse
|
# via reuse
|
||||||
pyyaml==6.0.2
|
python-magic==0.4.27
|
||||||
|
# via reuse
|
||||||
|
pyyaml==6.0.3
|
||||||
# via bandit
|
# via bandit
|
||||||
readme-renderer==44.0
|
readme-renderer==44.0
|
||||||
# via twine
|
# via twine
|
||||||
regex==2024.11.6
|
regex==2026.1.15
|
||||||
# via nltk
|
# via nltk
|
||||||
requests==2.32.3
|
requests==2.32.5
|
||||||
# via
|
# via
|
||||||
# id
|
# id
|
||||||
# requests-toolbelt
|
# requests-toolbelt
|
||||||
@@ -227,64 +241,72 @@ requests==2.32.3
|
|||||||
# twine
|
# twine
|
||||||
requests-toolbelt==1.0.0
|
requests-toolbelt==1.0.0
|
||||||
# via twine
|
# via twine
|
||||||
reuse==5.0.2
|
reuse==6.2.0
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
rfc3986==2.0.0
|
rfc3986==2.0.0
|
||||||
# via twine
|
# via twine
|
||||||
rich==13.9.4
|
rich==14.3.1
|
||||||
# via
|
# via
|
||||||
# bandit
|
# bandit
|
||||||
# twine
|
# twine
|
||||||
# typer
|
# typer
|
||||||
ruamel-yaml==0.18.10
|
ruamel-yaml==0.19.1
|
||||||
# via
|
# via
|
||||||
# safety
|
# safety
|
||||||
# safety-schemas
|
# safety-schemas
|
||||||
ruamel-yaml-clib==0.2.12
|
safety==3.7.0
|
||||||
# via ruamel-yaml
|
|
||||||
safety==3.3.1
|
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
safety-schemas==0.0.11
|
safety-schemas==0.0.16
|
||||||
# via safety
|
# via safety
|
||||||
secretstorage==3.3.3
|
secretstorage==3.5.0
|
||||||
# via keyring
|
# via keyring
|
||||||
setuptools-scm==8.2.0
|
setuptools-scm==9.2.2
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
shellingham==1.5.4
|
shellingham==1.5.4
|
||||||
# via typer
|
# via typer
|
||||||
six==1.17.0
|
six==1.17.0
|
||||||
# via python-dateutil
|
# via python-dateutil
|
||||||
snowballstemmer==2.2.0
|
snowballstemmer==3.0.1
|
||||||
# via pydocstyle
|
# via pydocstyle
|
||||||
stevedore==5.4.1
|
soupsieve==2.8.3
|
||||||
|
# via beautifulsoup4
|
||||||
|
stevedore==5.6.0
|
||||||
# via bandit
|
# via bandit
|
||||||
termcolor==2.5.0
|
tenacity==9.1.2
|
||||||
|
# via safety
|
||||||
|
termcolor==3.3.0
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
tomlkit==0.13.2
|
tomlkit==0.14.0
|
||||||
# via reuse
|
# via
|
||||||
tox==4.24.2
|
# reuse
|
||||||
|
# safety
|
||||||
|
tox==4.34.1
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
tqdm==4.67.1
|
tqdm==4.67.1
|
||||||
# via nltk
|
# via nltk
|
||||||
twine==6.1.0
|
twine==6.2.0
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
typer==0.15.2
|
typer==0.21.1
|
||||||
# via safety
|
# via safety
|
||||||
typing-extensions==4.12.2
|
typing-extensions==4.15.0
|
||||||
# via
|
# via
|
||||||
|
# beautifulsoup4
|
||||||
# mypy
|
# mypy
|
||||||
# pydantic
|
# pydantic
|
||||||
# pydantic-core
|
# pydantic-core
|
||||||
# safety
|
# safety
|
||||||
# safety-schemas
|
# safety-schemas
|
||||||
# typer
|
# typer
|
||||||
urllib3==2.3.0
|
# typing-inspection
|
||||||
|
typing-inspection==0.4.2
|
||||||
|
# via pydantic
|
||||||
|
urllib3==2.6.3
|
||||||
# via
|
# via
|
||||||
# requests
|
# requests
|
||||||
# twine
|
# twine
|
||||||
virtualenv==20.29.3
|
virtualenv==20.36.1
|
||||||
# via tox
|
# via tox
|
||||||
wheel==0.45.1
|
wheel==0.46.3
|
||||||
# via pip-tools
|
# via pip-tools
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
# The following packages are considered to be unsafe in a requirements 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:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile --output-file=requirements/requirements.txt
|
# pip-compile --output-file=requirements/requirements.txt
|
||||||
#
|
#
|
||||||
|
beautifulsoup4==4.14.3
|
||||||
|
# via incorporeal-cms (pyproject.toml)
|
||||||
feedgen==1.0.0
|
feedgen==1.0.0
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
lxml==5.3.1
|
lxml==6.0.2
|
||||||
# via feedgen
|
# via feedgen
|
||||||
markdown==3.7
|
markdown==3.10.1
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
markupsafe==3.0.2
|
markupsafe==3.0.3
|
||||||
# via jinja2
|
# via jinja2
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
# via feedgen
|
# via feedgen
|
||||||
six==1.17.0
|
six==1.17.0
|
||||||
# via python-dateutil
|
# via python-dateutil
|
||||||
termcolor==2.5.0
|
soupsieve==2.8.3
|
||||||
|
# via beautifulsoup4
|
||||||
|
termcolor==3.3.0
|
||||||
# via incorporeal-cms (pyproject.toml)
|
# via incorporeal-cms (pyproject.toml)
|
||||||
|
typing-extensions==4.15.0
|
||||||
|
# via beautifulsoup4
|
||||||
|
|||||||
@@ -23,13 +23,11 @@ def test_graphviz_is_rendered():
|
|||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
src_dir = os.path.join(HERE, 'instance')
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
ssg = StaticSiteGenerator(src_dir, tmpdir)
|
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)
|
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:
|
with open(os.path.join(tmpdir, 'test-graphviz.html'), 'r') as graphviz_output:
|
||||||
data = graphviz_output.read()
|
data = graphviz_output.read()
|
||||||
assert 'data:image/png;base64' in data
|
assert 'data:image/png;base64' in data
|
||||||
os.chdir(HERE)
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_graphviz_is_not_rendered():
|
def test_invalid_graphviz_is_not_rendered():
|
||||||
@@ -37,12 +35,10 @@ def test_invalid_graphviz_is_not_rendered():
|
|||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
src_dir = os.path.join(HERE, 'instance')
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
ssg = StaticSiteGenerator(src_dir, tmpdir)
|
ssg = StaticSiteGenerator(src_dir, tmpdir)
|
||||||
os.chdir(os.path.join(src_dir, 'broken'))
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ssg.build_file_in_destination(os.path.join(HERE, 'instance', 'broken'), '', 'test-invalid-graphviz.md',
|
ssg.build_file_in_destination(os.path.join(HERE, 'instance', 'broken'), '', 'test-invalid-graphviz.md',
|
||||||
tmpdir, True)
|
tmpdir, True)
|
||||||
os.chdir(HERE)
|
|
||||||
|
|
||||||
|
|
||||||
def test_figures_are_rendered():
|
def test_figures_are_rendered():
|
||||||
@@ -50,7 +46,6 @@ def test_figures_are_rendered():
|
|||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
src_dir = os.path.join(HERE, 'instance')
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
ssg = StaticSiteGenerator(src_dir, tmpdir)
|
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)
|
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:
|
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" />'
|
assert ('<figure class="left"><img alt="fancy logo" src="bss-square-no-bg.png" />'
|
||||||
'<span></span></figure>') in data
|
'<span></span></figure>') in data
|
||||||
assert '<figure><img alt="just a logo" src="bss-square-no-bg.png" /></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():
|
def test_og_image():
|
||||||
@@ -70,7 +64,6 @@ def test_og_image():
|
|||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
src_dir = os.path.join(HERE, 'instance')
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
ssg = StaticSiteGenerator(src_dir, tmpdir)
|
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)
|
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:
|
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:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
src_dir = os.path.join(HERE, 'instance')
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
ssg = StaticSiteGenerator(src_dir, tmpdir)
|
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
|
# 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
|
# the generated URLs for content in the pages/ root
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Redirect: http://www.google.com/
|
|
||||||
@@ -18,6 +18,10 @@
|
|||||||
"level": "DEBUG",
|
"level": "DEBUG",
|
||||||
"handlers": ["console"]
|
"handlers": ["console"]
|
||||||
},
|
},
|
||||||
|
"incorporealcms.markdown": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"handlers": ["console"]
|
||||||
|
},
|
||||||
"incorporealcms.ssg": {
|
"incorporealcms.ssg": {
|
||||||
"level": "DEBUG",
|
"level": "DEBUG",
|
||||||
"handlers": ["console"]
|
"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/no-title-or-h1.md
Normal file
1
tests/instance/pages/no-title-or-h1.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
there's just some words here but no title tag or h1
|
||||||
1
tests/instance/pages/no-title-subdir/index.md
Normal file
1
tests/instance/pages/no-title-subdir/index.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
there's just some words here but no title tag or h1
|
||||||
1
tests/instance/pages/no-title-subdir/no-title-or-h1.md
Normal file
1
tests/instance/pages/no-title-subdir/no-title-or-h1.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
there's just some words here but no title tag or h1
|
||||||
6
tests/instance/pages/rambling.md
Normal file
6
tests/instance/pages/rambling.md
Normal 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
|
||||||
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,71 +8,82 @@ from unittest.mock import patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
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__))
|
||||||
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():
|
def test_generate_page_navs_index():
|
||||||
"""Test that the index page has navs to the root (itself)."""
|
"""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_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') == [('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') == [('example.org', '/'), ('subdir', '/subdir/'),
|
assert generate_parent_navs('subdir/page.md', PAGES_DIR) == [('example.org', '/'),
|
||||||
|
('another page', '/subdir/'),
|
||||||
('Page', '/subdir/page')]
|
('Page', '/subdir/page')]
|
||||||
|
|
||||||
|
|
||||||
def test_generate_page_navs_subdir_with_title_parsing_real_page():
|
def test_generate_page_navs_subdir_with_title_parsing_real_page():
|
||||||
"""Test that title metadata is used in the nav text."""
|
"""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', '/'),
|
('example.org', '/'),
|
||||||
('SUB!', '/subdir-with-title/'),
|
('SUB!', '/subdir-with-title/'),
|
||||||
('page', '/subdir-with-title/page')
|
('/page', '/subdir-with-title/page')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_generate_page_navs_subdir_with_no_index():
|
def test_generate_page_navs_subdir_with_no_index():
|
||||||
"""Test that breadcrumbs still generate even if a subdir doesn't have an index.md."""
|
"""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', '/'),
|
('example.org', '/'),
|
||||||
('/no-index-dir/', '/no-index-dir/'),
|
('/no-index-dir/', '/no-index-dir/'),
|
||||||
('page', '/no-index-dir/page')
|
('/page', '/no-index-dir/page')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_page_includes_themes_with_default():
|
def test_page_includes_themes_with_default():
|
||||||
"""Test that a request contains the configured themes and sets the default as appropriate."""
|
"""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">'\
|
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">'\
|
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>'\
|
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>'\
|
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():
|
def test_render_with_style_overrides():
|
||||||
"""Test that the default can be changed."""
|
"""Test that the default can be changed."""
|
||||||
with patch('incorporealcms.Config.DEFAULT_PAGE_STYLE', 'dark'):
|
with patch('incorporealcms.Config.DEFAULT_PAGE_STYLE', 'dark'):
|
||||||
assert '<link rel="stylesheet" type="text/css" title="dark" href="/static/css/dark.css">'\
|
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">'\
|
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>'\
|
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>'\
|
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():
|
def test_render_with_default_style_override():
|
||||||
@@ -81,23 +92,15 @@ def test_render_with_default_style_override():
|
|||||||
'warm': '/static/css/warm.css'}):
|
'warm': '/static/css/warm.css'}):
|
||||||
with patch('incorporealcms.Config.DEFAULT_PAGE_STYLE', 'warm'):
|
with patch('incorporealcms.Config.DEFAULT_PAGE_STYLE', 'warm'):
|
||||||
assert '<link rel="stylesheet" type="text/css" title="warm" href="/static/css/warm.css">'\
|
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">'\
|
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">'\
|
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>'\
|
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>'\
|
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/'))
|
|
||||||
|
|
||||||
|
|
||||||
def test_instance_resource_path_to_request_path_on_index():
|
def test_instance_resource_path_to_request_path_on_index():
|
||||||
@@ -120,43 +123,103 @@ 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, mtime = parse_md('more-metadata.md')
|
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_name == 'title for the page'
|
||||||
assert page_title == 'title for the page - example.org'
|
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():
|
def test_parse_md_metadata_forced_no_title():
|
||||||
"""Test the direct results of parsing a markdown file."""
|
"""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_name == ''
|
||||||
assert page_title == 'example.org'
|
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."""
|
"""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_name == 'another page'
|
||||||
assert page_title == '/subdir/ - example.org'
|
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():
|
def test_parse_md_no_file():
|
||||||
"""Test the direct results of parsing a markdown file."""
|
"""Test the direct results of parsing a markdown file."""
|
||||||
with pytest.raises(FileNotFoundError):
|
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():
|
def test_parse_md_bad_file():
|
||||||
"""Test the direct results of parsing a markdown file."""
|
"""Test the direct results of parsing a markdown file."""
|
||||||
with pytest.raises(ValueError):
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
@@ -3,12 +3,15 @@
|
|||||||
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
|
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
"""
|
"""
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import incorporealcms.ssg as ssg
|
import incorporealcms.ssg as ssg
|
||||||
from incorporealcms import init_instance
|
from incorporealcms import init_instance
|
||||||
|
|
||||||
|
logger = logging.getLogger(__file__)
|
||||||
|
|
||||||
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')
|
||||||
@@ -20,7 +23,6 @@ def test_file_copy():
|
|||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
src_dir = os.path.join(HERE, 'instance')
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
|
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,
|
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'no-title.md', tmpdir,
|
||||||
True)
|
True)
|
||||||
assert os.path.exists(os.path.join(tmpdir, 'no-title.md'))
|
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:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
src_dir = os.path.join(HERE, 'instance')
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
|
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,
|
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'no-title.md', tmpdir,
|
||||||
False)
|
False)
|
||||||
assert os.path.exists(os.path.join(tmpdir, 'no-title.md'))
|
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:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
src_dir = os.path.join(HERE, 'instance')
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
|
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)
|
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
|
# 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)
|
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'))
|
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():
|
def test_file_copy_symlink_of_markdown_also_has_html_symlink():
|
||||||
"""Test the ability to sync source and generated symlinks to the output dir."""
|
"""Test the ability to sync source and generated symlinks to the output dir."""
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
src_dir = os.path.join(HERE, 'instance')
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
|
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,
|
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'symlink-to-no-title.md', tmpdir,
|
||||||
True)
|
True)
|
||||||
# need to copy the destination for os.path.exists to be happy with this
|
# 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:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
src_dir = os.path.join(HERE, 'instance')
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
|
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)
|
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.exists(os.path.join(tmpdir, 'media'))
|
||||||
assert os.path.isdir(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:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
src_dir = os.path.join(HERE, 'instance')
|
src_dir = os.path.join(HERE, 'instance')
|
||||||
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
|
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)
|
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
|
# 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)
|
generator.build_subdir_in_destination(os.path.join(instance_dir, 'pages'), '', 'subdir', tmpdir)
|
||||||
@@ -115,3 +129,26 @@ def test_build_in_destination_ignores_dot_files():
|
|||||||
generator.build_in_destination(os.path.join(src_dir, 'pages'), tmpdir)
|
generator.build_in_destination(os.path.join(src_dir, 'pages'), tmpdir)
|
||||||
|
|
||||||
assert not os.path.exists(os.path.join(tmpdir, '.ignored-file.md'))
|
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'))
|
||||||
|
|||||||
12
tox.ini
12
tox.ini
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
[tox]
|
[tox]
|
||||||
isolated_build = true
|
isolated_build = true
|
||||||
envlist = begin,py39,py310,py311,py312,coverage,security,lint,reuse
|
envlist = begin,py310,py311,py312,py313,coverage,security,lint,reuse
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
allow_externals = pytest, coverage
|
allow_externals = pytest, coverage
|
||||||
@@ -21,11 +21,6 @@ deps = setuptools
|
|||||||
skip_install = true
|
skip_install = true
|
||||||
commands = coverage erase
|
commands = coverage erase
|
||||||
|
|
||||||
[testenv:py39]
|
|
||||||
# run pytest with coverage
|
|
||||||
commands =
|
|
||||||
pytest --cov-append --cov={envsitepackagesdir}/incorporealcms/ --cov-branch
|
|
||||||
|
|
||||||
[testenv:py310]
|
[testenv:py310]
|
||||||
# run pytest with coverage
|
# run pytest with coverage
|
||||||
commands =
|
commands =
|
||||||
@@ -41,6 +36,11 @@ commands =
|
|||||||
commands =
|
commands =
|
||||||
pytest --cov-append --cov={envsitepackagesdir}/incorporealcms/ --cov-branch
|
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]
|
[testenv:coverage]
|
||||||
# report on coverage runs from above
|
# report on coverage runs from above
|
||||||
skip_install = true
|
skip_install = true
|
||||||
|
|||||||
Reference in New Issue
Block a user