Compare commits

..

16 Commits

Author SHA1 Message Date
7205bb2aa5
changelog for v2.0.4
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-21 11:48:20 -05:00
88b678931e
properly handle symlinks as relative to the output dir
the code meant to make symlinks relative to the output directory, but
only actually succeeded at it for targets in the root of the output
directory; since we already check that the target is not breaking out of
the instance, we can generate the output symlink relative to itself and
fix the subdirred-symlink behavior

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-21 11:43:18 -05:00
8c75947088
remove os.chdir usage, rely on absolute and relative paths more
os.chdir was getting confusing and hurting the log output, and
potentially the cause of a couple bugs left to fix, so this removes it,
but it means we need to pass around the pages/ absolute path into the
markdown parser, because it relies on knowing both the absolute path
now (to open files), and also the path relative to the pages dir in
order to know where to stop reading parent files/how to generate proper
URL-like references to other files.

probably this should be refactored at some point to inherit the pages/
path from the SSG somehow, rather than passing it through a bunch of
methods, but this seems to work for now

fixes #22

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-21 10:47:42 -05:00
e75d5c48d2
changelog for v2.0.3, though I forgot to put it in the tag
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-21 08:36:29 -05:00
201cd80804
when symlinking foo.md, also symlink foo.html if generating .html
fixes #24

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-21 08:31:31 -05:00
a327c6b89c
remove the Flask classifier, since we don't do it anymore
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-21 08:23:31 -05:00
27d4d16572
go back to using the "or any later version" clause of GPLv3
I think arguments that "Such new versions will be similar in spirit to
the present version", in my own reading and readings such as in
https://www.draketo.de/software/gpl-or-later, convince me for now that
it is acceptable to allow the "or later" for compatibility and future
problem's sake

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-21 08:22:49 -05:00
b69bdb424a
CHANGELOG for v2.0.2
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-18 21:28:15 -05:00
c46a1c0bae
don't handle custom-static dirs specially anymore
they're just more static files among all the static files we serve, they
should go into pages/ like everything else

fixes #20

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-18 21:26:25 -05:00
359916e7d9
do some trivial cleanups in README
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-18 21:20:12 -05:00
d49b9d48a8
don't add an artificial ./ subdir due to how os.path.relpath works
this fixes stuff like og:urls of https://foo/./ or https://foo/./page
and also removes an extra layer of depth in the breadcrumb hierarchy,
just by suppressing the '.' in relpath output at the root of pages/

fixes #21

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-18 21:09:50 -05:00
9caf08a277
changelog for v2.0.1
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-18 08:24:46 -05:00
8aabd93273
don't copy .files into the SSG output dir
they may be vim swap files and that kind of garbage

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-18 08:20:58 -05:00
6d7987cfae
don't require the host to be in the Image tag
now that we know our base host via config, we can stop hardcoding it in
each Image tag

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-18 08:13:18 -05:00
abc05ee4e8
fix how my email address displays in gitea
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-17 13:22:30 -05:00
f623ffdd7c
don't refer to my manual uploads now that I push to PyPI
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-03-17 13:13:25 -05:00
37 changed files with 298 additions and 173 deletions

View File

@ -1,24 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: incorporeal-cms
Upstream-Contact: Brian S. Stephan <bss@incorporeal.org>
Source: https://git.incorporeal.org/bss/incorporeal-cms
# Trivial files
Files: .gitignore .gitattributes
Copyright: © 2020 Brian S. Stephan <bss@incorporeal.org>
License: GPL-3.0-only
# High level repo docs
Files: *.md
Copyright: © 2020 Brian S. Stephan <bss@incorporeal.org>
License: GPL-3.0-only
# Test data
Files: tests/instance/*
Copyright: © 2020 Brian S. Stephan <bss@incorporeal.org>
License: GPL-3.0-only
# Python packaging, scaffolding, and errata
Files: MANIFEST.in pyproject.toml tox.ini requirements/*
Copyright: © 2020 Brian S. Stephan <bss@incorporeal.org>
License: GPL-3.0-only

View File

@ -2,6 +2,46 @@
Included is a summary of changes to the project, by version. Details can be found in the commit history.
## v2.0.4
### Bugfixes
* With some significant refactoring, files are now handled better with respect to relative paths, which fixes an issue
with symlink pages only properly getting resolved to their target if the symlink was in the `pages/` root rather than
a subdir.
## v2.0.3
### Bugfixes
* Symlinks for a `.md` file that are to be served by the web server also need a `.html` symlink pointed to the generated
file, since the web server is looking for HTML files when serving paths.
### Miscellaneous
* The project now comes with the GPLv3 "or any later version" clause.
## v2.0.2
### Bugfixes
* Paths for files in the `pages/` root no longer have an extra `./` in them, which made URLs look ugly and also added an
extra blank breadcrumb in the breadcrumbs.
### Improvements
* `custom-static` in the instance dir is now ignored and has no special handling --- put static files in `pages/static/`
like all the other files that get copied. This also fixes a bug where the build errored if the directory didn't exist.
* Some README typos fixed.
## v2.0.1
### Improvements
* The `Image` tag in Markdown files no longer requires the full URL to be specified. Now `Config.BASE_HOST` is
prepended to the tag value, which should be the full path to the image.
* `.files` are skipped when copying files to the SSG output directory.
## v2.0.0
### Features

View File

@ -4,8 +4,7 @@ A lightweight static site generator for Markdown-based sites.
## Installation and Usage
I recommend getting a release from <https://git.incorporeal.org/bss/incorporeal-cms/releases> and
installing the Python package in a virtualenv. Something like the following should suffice:
Something like the following should suffice:
```
% virtualenv --python=python3.9 env-py3.9
@ -15,19 +14,19 @@ installing the Python package in a virtualenv. Something like the following shou
% incorporealcms-build ./path/to/instance ./path/to/output/www/root
```
This will generate the directory suitable for serving by e.g. Nginx.
This will generate the directory suitable for serving by e.g. nginx.
## Creating a Site
Put content, notably Markdown content, inside `./your-instance/pages/` and when you are ready, run the build command
above. When you run `incorporealcms-build`, the following happens:
* Markdown files (ending in `.md`) are rendered via Python-Markdown as .html files and output to the static site
* Markdown files (ending in `.md`) are rendered via Python-Markdown as `.html` files and output to the static site
directory. The `.md` files are also copied there, though this behavior may be toggleable in the future.
* Directory paths (e.g. a request to `/dir/`) can be served via a `/dir/index.md` file, which will generate
`/dir/index.html`, with the appropriate web server configuration to use `index.html` for directory listings.
* Symlinks to files are retained and mirrored into the output directory, and handled per the web server's configuration,
whanever it is.
whatever it is.
* All other files are copied directly, so images, text files, etc., can be referenced naturally as URLs.
## Configuration
@ -53,19 +52,18 @@ out and discuss issues and features and whatnot.
## Author and Licensing
Written by and copyright (C) 2025 Brian S. Stephan <bss@incorporeal.org>.
Written by and copyright (C) 2025 Brian S. Stephan (bss@incorporeal.org).
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, version 3 of the License.
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General
Public License as published by the Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
You should have received a copy of the GNU General Public License along with this program. If not, see
<https://www.gnu.org/licenses/>.
### Content Output

28
REUSE.toml Normal file
View File

@ -0,0 +1,28 @@
version = 1
SPDX-PackageName = "incorporeal-cms"
SPDX-PackageSupplier = "Brian S. Stephan <bss@incorporeal.org>"
SPDX-PackageDownloadLocation = "https://git.incorporeal.org/bss/incorporeal-cms"
[[annotations]]
path = [".gitignore", ".gitattributes"]
precedence = "aggregate"
SPDX-FileCopyrightText = "© 2020 Brian S. Stephan <bss@incorporeal.org>"
SPDX-License-Identifier = "GPL-3.0-or-later"
[[annotations]]
path = "**.md"
precedence = "aggregate"
SPDX-FileCopyrightText = "© 2020 Brian S. Stephan <bss@incorporeal.org>"
SPDX-License-Identifier = "GPL-3.0-or-later"
[[annotations]]
path = "tests/instance/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "© 2020 Brian S. Stephan <bss@incorporeal.org>"
SPDX-License-Identifier = "GPL-3.0-or-later"
[[annotations]]
path = ["MANIFEST.in", "pyproject.toml", "tox.ini", "requirements/**"]
precedence = "aggregate"
SPDX-FileCopyrightText = "© 2020 Brian S. Stephan <bss@incorporeal.org>"
SPDX-License-Identifier = "GPL-3.0-or-later"

View File

@ -1,7 +1,7 @@
"""An application for running my Markdown-based sites.
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""
import json
import logging

View File

@ -1,7 +1,7 @@
"""Default configuration.
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""

View File

@ -1,7 +1,7 @@
"""Process the error page templates.
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""
import os

View File

@ -7,7 +7,7 @@ under pages/ (which may make sense for a blog) if they want, but could just
as well be pages/foo content.
SPDX-FileCopyrightText: © 2023 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""
import logging
import os
@ -24,6 +24,9 @@ logger = logging.getLogger(__name__)
def generate_feed(feed_type: str, instance_dir: str, dest_dir: str) -> None:
"""Generate the Atom or RSS feed as requested.
Feed entries should be symlinks to .md files in the pages/ directory, so that they
are also linkable and can be browsed outside of the feed.
Args:
feed_type: 'atom' or 'rss' feed
instance_dir: the directory for the instance, containing both the feed dir and pages
@ -37,16 +40,18 @@ def generate_feed(feed_type: str, instance_dir: str, dest_dir: str) -> None:
fg.link(href=f'https://{Config.DOMAIN_NAME}', rel='alternate')
fg.subtitle(f"Blog posts and other interesting materials from {Config.TITLE_SUFFIX}")
# feed symlinks should all be within the core content subdirectory
pages_dir = os.path.join(instance_dir, 'pages')
# get recent feeds
feed_path = os.path.join(instance_dir, 'feed')
feed_entry_paths = [os.path.join(dirpath, filename) for dirpath, _, filenames in os.walk(feed_path)
for filename in filenames if os.path.islink(os.path.join(dirpath, filename))]
for feed_entry_path in sorted(feed_entry_paths):
# get the actual file to parse it
os.chdir(os.path.abspath(os.path.join(instance_dir, 'pages')))
resolved_path = os.path.relpath(os.path.realpath(feed_entry_path), os.path.join(instance_dir, 'pages'))
resolved_path = os.path.relpath(os.path.realpath(feed_entry_path), pages_dir)
try:
content, md, page_name, page_title, mtime = parse_md(resolved_path)
content, md, page_name, page_title, mtime = parse_md(os.path.join(pages_dir, resolved_path), pages_dir)
link = f'https://{Config.DOMAIN_NAME}{instance_resource_path_to_request_path(resolved_path)}'
except (OSError, ValueError, TypeError):
logger.exception("error loading/rendering markdown!")

View File

@ -5,7 +5,7 @@ as is, but .md files need to be processed with a Markdown parser, so a lot of th
is our tweaks and customizations for pages my way.
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""
import datetime
import logging
@ -44,18 +44,21 @@ def instance_resource_path_to_request_path(path):
return '/' + re.sub(r'.md$', '', re.sub(r'index.md$', '', path))
def parse_md(path: str):
def parse_md(path: str, pages_root: str):
"""Given a file to parse, return file content and other derived data along with the md object.
Args:
path: the path to the file to render
pages_root: the absolute path to the pages/ dir, which the path should be within. necessary for
proper resolution of resolving parent pages (which needs to know when to stop)
"""
try:
logger.debug("opening path '%s'", path)
with open(path, 'r') as input_file:
absolute_path = os.path.join(pages_root, path)
logger.debug("opening path '%s'", absolute_path)
with open(absolute_path, 'r') as input_file:
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(input_file.name), tz=datetime.timezone.utc)
entry = input_file.read()
logger.debug("path '%s' read", path)
logger.debug("path '%s' read", absolute_path)
md = init_md()
content = Markup(md.convert(entry)) # nosec B704
except (OSError, FileNotFoundError):
@ -67,17 +70,25 @@ def parse_md(path: str):
logger.debug("file metadata: %s", md.Meta)
page_name = get_meta_str(md, 'title') if md.Meta.get('title') else instance_resource_path_to_request_path(path)
rel_path = os.path.relpath(path, pages_root)
page_name = get_meta_str(md, 'title') if md.Meta.get('title') else instance_resource_path_to_request_path(rel_path)
page_title = f'{page_name} - {Config.TITLE_SUFFIX}' if page_name else Config.TITLE_SUFFIX
logger.debug("title (potentially derived): %s", page_title)
return content, md, page_name, page_title, mtime
def handle_markdown_file_path(path: str) -> str:
"""Given a location on disk, attempt to open it and render the markdown within."""
content, md, page_name, page_title, mtime = parse_md(path)
parent_navs = generate_parent_navs(path)
def handle_markdown_file_path(path: str, pages_root: str) -> str:
"""Given a location on disk, attempt to open it and render the markdown within.
Args:
path: the path to the file to parse and produce metadata for
pages_root: the absolute path to the pages/ dir, which the path should be within. necessary for
proper resolution of resolving parent pages (which needs to know when to stop)
"""
content, md, page_name, page_title, mtime = parse_md(path, pages_root)
relative_path = os.path.relpath(path, pages_root)
parent_navs = generate_parent_navs(relative_path, pages_root)
extra_footer = get_meta_str(md, 'footer') if md.Meta.get('footer') else None
template_name = get_meta_str(md, 'template') if md.Meta.get('template') else 'base.html'
@ -90,16 +101,23 @@ def handle_markdown_file_path(path: str) -> str:
return template.render(title=page_title,
config=Config,
description=get_meta_str(md, 'description'),
image=get_meta_str(md, 'image'),
image=Config.BASE_HOST + get_meta_str(md, 'image'),
content=content,
base_url=Config.BASE_HOST + instance_resource_path_to_request_path(path),
base_url=Config.BASE_HOST + instance_resource_path_to_request_path(relative_path),
navs=parent_navs,
mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z'),
extra_footer=extra_footer)
def generate_parent_navs(path):
"""Create a series of paths/links to navigate up from the given resource path."""
def generate_parent_navs(path, pages_root: str):
"""Create a series of paths/links to navigate up from the given resource path.
Args:
path: the path to parse and generate parent metadata nav links for
pages_root: the absolute path to the pages/ dir, which the path should be within. path is relative,
but opening parents requires the full path
"""
logger.debug("path to generate navs for: %s", path)
if path == 'index.md':
# bail and return the domain name as a terminal case
return [(Config.DOMAIN_NAME, '/')]
@ -124,14 +142,14 @@ def generate_parent_navs(path):
# read the resource
try:
with open(path, 'r') as entry_file:
with open(os.path.join(pages_root, path), 'r') as entry_file:
entry = entry_file.read()
_ = Markup(md.convert(entry)) # nosec B704
page_name = (" ".join(md.Meta.get('title')) if md.Meta.get('title')
else request_path_to_breadcrumb_display(request_path))
return generate_parent_navs(parent_resource_path) + [(page_name, request_path)]
return generate_parent_navs(parent_resource_path, pages_root) + [(page_name, request_path)]
except FileNotFoundError:
return generate_parent_navs(parent_resource_path) + [(request_path, request_path)]
return generate_parent_navs(parent_resource_path, pages_root) + [(request_path, request_path)]
def request_path_to_breadcrumb_display(path):

View File

@ -1,5 +1,5 @@
"""Markdown extensions.
SPDX-FileCopyrightText: © 2021 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""

View File

@ -1,7 +1,7 @@
"""Create generic figures with captions.
SPDX-FileCopyrightText: © 2022 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""
import re
from xml.etree.ElementTree import SubElement # nosec B405

View File

@ -1,7 +1,7 @@
"""Serve dot diagrams inline.
SPDX-FileCopyrightText: © 2021 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""
import base64
import logging

View File

@ -1,9 +1,10 @@
"""Build an instance as a static site suitable for serving via e.g. Nginx.
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""
import argparse
import logging
import os
import shutil
import stat
@ -16,6 +17,8 @@ from incorporealcms.error_pages import generate_error_pages
from incorporealcms.feed import generate_feed
from incorporealcms.markdown import handle_markdown_file_path
logger = logging.getLogger(__name__)
class StaticSiteGenerator(object):
"""Generate static site output based on the instance's content."""
@ -53,10 +56,6 @@ class StaticSiteGenerator(object):
pass
self.build_in_destination(program_static_dir, static_output_dir, convert_markdown=False)
# copy the instance's static dir --- should I deprecate this since it could just be stuff in pages/static/?
custom_static_dir = os.path.join(self.instance_dir, 'custom-static')
self.build_in_destination(custom_static_dir, static_output_dir, convert_markdown=False)
# generate the feeds
cprint("generating feeds", 'green')
generate_feed('atom', self.instance_dir, tmp_output_dir)
@ -87,16 +86,20 @@ class StaticSiteGenerator(object):
convert_markdown: whether or not to convert Markdown files (or simply copy them)
"""
cprint(f"copying files from '{source_dir}' to '{dest_dir}'", 'green')
os.chdir(source_dir)
for base_dir, subdirs, files in os.walk(source_dir):
logger.debug("starting to build against %s || %s || %s", base_dir, subdirs, files)
# remove the absolute path of the directory from the base_dir
base_dir = os.path.relpath(base_dir, source_dir)
relpath = os.path.relpath(base_dir, source_dir)
base_dir = relpath if relpath != '.' else ''
# create subdirs seen here for subsequent depth
for subdir in subdirs:
self.build_subdir_in_destination(source_dir, base_dir, subdir, dest_dir)
# process and copy files
for file_ in files:
if file_[0] == '.':
cprint(f"skipping {file_}", 'yellow')
continue
self.build_file_in_destination(source_dir, base_dir, file_, dest_dir, convert_markdown)
def build_subdir_in_destination(self, source_dir: str, base_dir: str, subdir: str, dest_dir: str) -> None:
@ -109,9 +112,12 @@ class StaticSiteGenerator(object):
dest_dir: the output directory to place the subdir in
"""
dst = os.path.join(dest_dir, base_dir, subdir)
if os.path.islink(os.path.join(base_dir, subdir)):
absolute_dir = os.path.join(source_dir, base_dir, subdir)
logger.debug("checking if %s is a symlink or not", absolute_dir)
if os.path.islink(absolute_dir):
logger.debug("symlink; raw destination is %s", os.path.realpath(absolute_dir))
# keep the link relative to the output directory
src = self.symlink_to_relative_dest(source_dir, os.path.join(base_dir, subdir))
src = self.symlink_to_relative_dest(source_dir, absolute_dir)
print(f"creating directory symlink '{dst}' -> '{src}'")
os.symlink(src, dst, target_is_directory=True)
else:
@ -133,21 +139,33 @@ class StaticSiteGenerator(object):
dest_dir: the output directory to place the subdir in
"""
dst = os.path.join(dest_dir, base_dir, file_)
if os.path.islink(os.path.join(base_dir, file_)):
absolute_file = os.path.join(source_dir, base_dir, file_)
logger.debug("checking if %s is a symlink or not", absolute_file)
if os.path.islink(absolute_file):
logger.debug("symlink; raw destination is %s", os.path.realpath(absolute_file))
# keep the link relative to the output directory
src = self.symlink_to_relative_dest(source_dir, os.path.join(base_dir, file_))
src = self.symlink_to_relative_dest(source_dir, absolute_file)
print(f"creating symlink '{dst}' -> '{src}'")
os.symlink(src, dst, target_is_directory=False)
if src.endswith('.md') and convert_markdown:
# we also need to make a .html symlink so that web server configs
# pick up the "redirect"
second_src = src.removesuffix('.md') + '.html'
second_dst = dst.removesuffix('.md') + '.html'
print(f"creating symlink '{second_dst}' -> '{second_src}'")
os.symlink(second_src, second_dst, target_is_directory=False)
else:
src = os.path.join(base_dir, file_)
src = os.path.join(source_dir, base_dir, file_)
print(f"copying file '{src}' -> '{dst}'")
shutil.copy2(src, dst)
# render markdown as HTML
if src.endswith('.md') and convert_markdown:
rendered_file = dst.removesuffix('.md') + '.html'
print(f"rendering file '{src}' -> '{rendered_file}'")
try:
content = handle_markdown_file_path(src)
content = handle_markdown_file_path(src, source_dir)
except UnicodeDecodeError:
# perhaps this isn't a markdown file at all for some reason; we
# copied it above so stick with tha
@ -159,6 +177,10 @@ class StaticSiteGenerator(object):
def symlink_to_relative_dest(self, base_dir: str, source: str) -> str:
"""Given a symlink, make sure it points to something inside the instance and provide its real destination.
This is made to be relative to the location of the symlink in all
circumstances, in order to avoid breaking out of the instance or output
dirs.
Args:
base_dir: the full absolute path of the instance's pages dir, which the symlink destination must be in.
source: the symlink to check
@ -167,8 +189,8 @@ class StaticSiteGenerator(object):
"""
if not os.path.realpath(source).startswith(base_dir):
raise ValueError(f"symlink destination {os.path.realpath(source)} is outside the instance!")
# this symlink points to realpath inside base_dir, so relative to base_dir, the symlink dest is...
return os.path.relpath(os.path.realpath(source), base_dir)
# this symlink points to realpath inside base_dir, so relative to the source, the symlink dest is...
return os.path.relpath(os.path.realpath(source), os.path.dirname(source))
def build():

View File

@ -1,6 +1,6 @@
/*
* SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
* SPDX-License-Identifier: GPL-3.0-only
* SPDX-License-Identifier: GPL-3.0-or-later
*/
html {

View File

@ -1,6 +1,6 @@
/*
* SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
* SPDX-License-Identifier: GPL-3.0-only
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/* common styling via the base.css, used in light and dark */

View File

@ -1,6 +1,6 @@
/*
* SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
* SPDX-License-Identifier: GPL-3.0-only
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/* common styling via the base.css, used in light and dark */

View File

@ -1,6 +1,6 @@
/*
* SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
* SPDX-License-Identifier: GPL-3.0-only
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/* specify almost no styling, just fix some image and nav rendering */

View File

@ -5,7 +5,7 @@
* of my CMS, so I want to keep it around even in the static site.
*
* SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
* SPDX-License-Identifier: GPL-3.0-only
* SPDX-License-Identifier: GPL-3.0-or-later
*/
/**

View File

@ -1,6 +1,6 @@
{#
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
#}
{% extends "base.html" %}

View File

@ -1,6 +1,6 @@
{#
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
#}
{% extends "base.html" %}

View File

@ -1,6 +1,6 @@
{#
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
#}
{% extends "base.html" %}

View File

@ -1,6 +1,6 @@
{#
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
#}
{% extends "base.html" %}
{% block site_class %}class="site-wrap site-wrap-double-width"{% endblock %}

View File

@ -1,13 +1,13 @@
<!doctype html>{#
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
#}
<html lang="en">
<title>{{ title }}</title>
<meta charset="utf-8">
<meta property="og:url" content="{{ base_url }}">
{% if title %}<meta property="og:title" content="{{ title }}">{% endif %}
{% if description %}<meta property="og:description" content="{{ description }}">{% endif %}
<meta property="og:url" content="{{ base_url }}">
{% if image %}<meta property="og:image" content="{{ image }}">{% endif %}
<meta name="twitter:card" content="summary_large_image">
<meta name="viewport" content="width=device-width, initial-scale=1">

View File

@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
name = "incorporeal-cms"
description = "A lightweight static site generator for Markdown-based sites."
readme = "README.md"
license = {text = "GPL-3.0-only"}
license = {text = "GPL-3.0-or-later"}
authors = [
{name = "Brian S. Stephan", email = "bss@incorporeal.org"},
]
@ -14,9 +14,8 @@ requires-python = ">=3.9"
dependencies = ["feedgen", "jinja2", "Markdown", "termcolor"]
dynamic = ["version"]
classifiers = [
"Framework :: Flask",
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: OS Independent",
"Topic :: Text Processing :: Markup :: Markdown",
]

View File

@ -1,7 +1,7 @@
"""Test graphviz functionality.
SPDX-FileCopyrightText: © 2021 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""
import os
import tempfile
@ -23,13 +23,11 @@ def test_graphviz_is_rendered():
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
ssg = StaticSiteGenerator(src_dir, tmpdir)
os.chdir(os.path.join(src_dir, 'pages'))
ssg.build_file_in_destination(os.path.join(HERE, 'instance', 'pages'), '', 'test-graphviz.md', tmpdir, True)
with open(os.path.join(tmpdir, 'test-graphviz.html'), 'r') as graphviz_output:
data = graphviz_output.read()
assert 'data:image/png;base64' in data
os.chdir(HERE)
def test_invalid_graphviz_is_not_rendered():
@ -37,12 +35,10 @@ def test_invalid_graphviz_is_not_rendered():
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
ssg = StaticSiteGenerator(src_dir, tmpdir)
os.chdir(os.path.join(src_dir, 'broken'))
with pytest.raises(ValueError):
ssg.build_file_in_destination(os.path.join(HERE, 'instance', 'broken'), '', 'test-invalid-graphviz.md',
tmpdir, True)
os.chdir(HERE)
def test_figures_are_rendered():
@ -50,7 +46,6 @@ def test_figures_are_rendered():
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
ssg = StaticSiteGenerator(src_dir, tmpdir)
os.chdir(os.path.join(src_dir, 'pages'))
ssg.build_file_in_destination(os.path.join(HERE, 'instance', 'pages'), '', 'figures.md', tmpdir, True)
with open(os.path.join(tmpdir, 'figures.html'), 'r') as graphviz_output:
@ -62,4 +57,29 @@ def test_figures_are_rendered():
assert ('<figure class="left"><img alt="fancy logo" src="bss-square-no-bg.png" />'
'<span></span></figure>') in data
assert '<figure><img alt="just a logo" src="bss-square-no-bg.png" /></figure>' in data
os.chdir(HERE)
def test_og_image():
"""Test that the og:image meta tag is present as expected."""
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
ssg = StaticSiteGenerator(src_dir, tmpdir)
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:
data = markdown_output.read()
assert ('<meta property="og:image" content="http://example.org/test.img">') in data
def test_og_url():
"""Test that the og:url meta tag is present as expected."""
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
ssg = StaticSiteGenerator(src_dir, tmpdir)
# 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
ssg.build_in_destination(os.path.join(HERE, 'instance', 'pages'), tmpdir, True)
with open(os.path.join(tmpdir, 'index.html'), 'r') as markdown_output:
data = markdown_output.read()
assert ('<meta property="og:url" content="http://example.org/">') in data

View File

@ -18,7 +18,11 @@
"level": "DEBUG",
"handlers": ["console"]
},
"incorporealcms.pages": {
"incorporealcms.markdown": {
"level": "DEBUG",
"handlers": ["console"]
},
"incorporealcms.ssg": {
"level": "DEBUG",
"handlers": ["console"]
}

View File

@ -1,31 +0,0 @@
"""Configure the test application.
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
"""
LOGGING = {
'version': 1,
'formatters': {
'default': {
'format': '[%(asctime)s %(levelname)-7s %(name)s] %(message)s',
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'default',
},
},
'loggers': {
'incorporealcms.mdx': {
'level': 'DEBUG',
'handlers': ['console'],
},
'incorporealcms.pages': {
'level': 'DEBUG',
'handlers': ['console'],
},
},
}

View File

@ -0,0 +1 @@
this is ignored

View File

@ -1,6 +1,6 @@
Title: title for the page
Description: description of this page
made even longer
Image: http://buh.com/test.img
Image: /test.img
hello

View File

@ -0,0 +1 @@
../more-metadata.md

View File

@ -1,7 +1,7 @@
"""Test command line invocations.
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""
import os
import tempfile

View File

@ -1,7 +1,7 @@
"""Test basic configuration stuff.
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""
import os

View File

@ -1,7 +1,7 @@
"""Test the feed methods.
SPDX-FileCopyrightText: © 2023 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""
import os
import tempfile

View File

@ -1,40 +1,45 @@
"""Test the conversion of Markdown pages.
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""
import os
from unittest.mock import patch
import pytest
from incorporealcms import init_instance
from incorporealcms.markdown import (generate_parent_navs, handle_markdown_file_path,
instance_resource_path_to_request_path, parse_md,
request_path_to_breadcrumb_display)
HERE = os.path.dirname(os.path.abspath(__file__))
os.chdir(os.path.join(HERE, 'instance/', 'pages/'))
INSTANCE_DIR = os.path.join(HERE, 'instance')
PAGES_DIR = os.path.join(INSTANCE_DIR, 'pages/')
# initialize in order to configure debug logging
init_instance(INSTANCE_DIR)
def test_generate_page_navs_index():
"""Test that the index page has navs to the root (itself)."""
assert generate_parent_navs('index.md') == [('example.org', '/')]
assert generate_parent_navs('index.md', PAGES_DIR) == [('example.org', '/')]
def test_generate_page_navs_subdir_index():
"""Test that dir pages have navs to the root and themselves."""
assert generate_parent_navs('subdir/index.md') == [('example.org', '/'), ('subdir', '/subdir/')]
assert generate_parent_navs('subdir/index.md', PAGES_DIR) == [('example.org', '/'), ('subdir', '/subdir/')]
def test_generate_page_navs_subdir_real_page():
"""Test that real pages have navs to the root, their parent, and themselves."""
assert generate_parent_navs('subdir/page.md') == [('example.org', '/'), ('subdir', '/subdir/'),
('Page', '/subdir/page')]
assert generate_parent_navs('subdir/page.md', PAGES_DIR) == [('example.org', '/'), ('subdir', '/subdir/'),
('Page', '/subdir/page')]
def test_generate_page_navs_subdir_with_title_parsing_real_page():
"""Test that title metadata is used in the nav text."""
assert generate_parent_navs('subdir-with-title/page.md') == [
assert generate_parent_navs('subdir-with-title/page.md', PAGES_DIR) == [
('example.org', '/'),
('SUB!', '/subdir-with-title/'),
('page', '/subdir-with-title/page')
@ -43,7 +48,7 @@ def test_generate_page_navs_subdir_with_title_parsing_real_page():
def test_generate_page_navs_subdir_with_no_index():
"""Test that breadcrumbs still generate even if a subdir doesn't have an index.md."""
assert generate_parent_navs('no-index-dir/page.md') == [
assert generate_parent_navs('no-index-dir/page.md', PAGES_DIR) == [
('example.org', '/'),
('/no-index-dir/', '/no-index-dir/'),
('page', '/no-index-dir/page')
@ -53,26 +58,26 @@ def test_generate_page_navs_subdir_with_no_index():
def test_page_includes_themes_with_default():
"""Test that a request contains the configured themes and sets the default as appropriate."""
assert '<link rel="stylesheet" type="text/css" title="light" href="/static/css/light.css">'\
in handle_markdown_file_path('index.md')
in handle_markdown_file_path('index.md', PAGES_DIR)
assert '<link rel="alternate stylesheet" type="text/css" title="dark" href="/static/css/dark.css">'\
in handle_markdown_file_path('index.md')
in handle_markdown_file_path('index.md', PAGES_DIR)
assert '<a href="" onclick="setStyle(\'light\'); return false;">[light]</a>'\
in handle_markdown_file_path('index.md')
in handle_markdown_file_path('index.md', PAGES_DIR)
assert '<a href="" onclick="setStyle(\'dark\'); return false;">[dark]</a>'\
in handle_markdown_file_path('index.md')
in handle_markdown_file_path('index.md', PAGES_DIR)
def test_render_with_style_overrides():
"""Test that the default can be changed."""
with patch('incorporealcms.Config.DEFAULT_PAGE_STYLE', 'dark'):
assert '<link rel="stylesheet" type="text/css" title="dark" href="/static/css/dark.css">'\
in handle_markdown_file_path('index.md')
in handle_markdown_file_path('index.md', PAGES_DIR)
assert '<link rel="alternate stylesheet" type="text/css" title="light" href="/static/css/light.css">'\
in handle_markdown_file_path('index.md')
in handle_markdown_file_path('index.md', PAGES_DIR)
assert '<a href="" onclick="setStyle(\'light\'); return false;">[light]</a>'\
in handle_markdown_file_path('index.md')
in handle_markdown_file_path('index.md', PAGES_DIR)
assert '<a href="" onclick="setStyle(\'dark\'); return false;">[dark]</a>'\
in handle_markdown_file_path('index.md')
in handle_markdown_file_path('index.md', PAGES_DIR)
def test_render_with_default_style_override():
@ -81,23 +86,21 @@ def test_render_with_default_style_override():
'warm': '/static/css/warm.css'}):
with patch('incorporealcms.Config.DEFAULT_PAGE_STYLE', 'warm'):
assert '<link rel="stylesheet" type="text/css" title="warm" href="/static/css/warm.css">'\
in handle_markdown_file_path('index.md')
in handle_markdown_file_path('index.md', PAGES_DIR)
assert '<link rel="alternate stylesheet" type="text/css" title="cool" href="/static/css/cool.css">'\
in handle_markdown_file_path('index.md')
in handle_markdown_file_path('index.md', PAGES_DIR)
assert '<link rel="alternate stylesheet" type="text/css" title="light" href="/static/css/light.css">'\
not in handle_markdown_file_path('index.md')
not in handle_markdown_file_path('index.md', PAGES_DIR)
assert '<a href="" onclick="setStyle(\'warm\'); return false;">[warm]</a>'\
in handle_markdown_file_path('index.md')
in handle_markdown_file_path('index.md', PAGES_DIR)
assert '<a href="" onclick="setStyle(\'cool\'); return false;">[cool]</a>'\
in handle_markdown_file_path('index.md')
in handle_markdown_file_path('index.md', PAGES_DIR)
def test_redirects_error_unsupported():
"""Test that we throw a warning about the barely-used Markdown redirect tag, which we can't support via SSG."""
os.chdir(os.path.join(HERE, 'instance/', 'broken/'))
with pytest.raises(NotImplementedError):
handle_markdown_file_path('redirect.md')
os.chdir(os.path.join(HERE, 'instance/', 'pages/'))
handle_markdown_file_path('redirect.md', os.path.join(INSTANCE_DIR, 'broken'))
def test_instance_resource_path_to_request_path_on_index():
@ -131,21 +134,21 @@ def test_request_path_to_breadcrumb_display_patterns():
def test_parse_md_metadata():
"""Test the direct results of parsing a markdown file."""
content, md, page_name, page_title, mtime = parse_md('more-metadata.md')
content, md, page_name, page_title, mtime = parse_md(os.path.join(PAGES_DIR, 'more-metadata.md'), PAGES_DIR)
assert page_name == 'title for the page'
assert page_title == 'title for the page - example.org'
def test_parse_md_metadata_forced_no_title():
"""Test the direct results of parsing a markdown file."""
content, md, page_name, page_title, mtime = parse_md('forced-no-title.md')
content, md, page_name, page_title, mtime = parse_md(os.path.join(PAGES_DIR, 'forced-no-title.md'), PAGES_DIR)
assert page_name == ''
assert page_title == 'example.org'
def test_parse_md_metadata_no_title_so_path():
"""Test the direct results of parsing a markdown file."""
content, md, page_name, page_title, mtime = parse_md('subdir/index.md')
content, md, page_name, page_title, mtime = parse_md(os.path.join(PAGES_DIR, 'subdir/index.md'), PAGES_DIR)
assert page_name == '/subdir/'
assert page_title == '/subdir/ - example.org'
@ -153,10 +156,10 @@ def test_parse_md_metadata_no_title_so_path():
def test_parse_md_no_file():
"""Test the direct results of parsing a markdown file."""
with pytest.raises(FileNotFoundError):
content, md, page_name, page_title, mtime = parse_md('nope.md')
content, md, page_name, page_title, mtime = parse_md(os.path.join(PAGES_DIR, 'nope.md'), PAGES_DIR)
def test_parse_md_bad_file():
"""Test the direct results of parsing a markdown file."""
with pytest.raises(ValueError):
content, md, page_name, page_title, mtime = parse_md('actually-a-png.md')
content, md, page_name, page_title, mtime = parse_md(os.path.join(PAGES_DIR, 'actually-a-png.md'), PAGES_DIR)

View File

@ -1,14 +1,17 @@
"""Test the high level SSG operations.
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
SPDX-License-Identifier: GPL-3.0-or-later
"""
import logging
import os
import tempfile
import incorporealcms.ssg as ssg
from incorporealcms import init_instance
logger = logging.getLogger(__file__)
HERE = os.path.dirname(os.path.abspath(__file__))
instance_dir = os.path.join(HERE, 'instance')
@ -20,7 +23,6 @@ def test_file_copy():
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
os.chdir(os.path.join(instance_dir, 'pages'))
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'no-title.md', tmpdir,
True)
assert os.path.exists(os.path.join(tmpdir, 'no-title.md'))
@ -32,7 +34,6 @@ def test_file_copy_no_markdown():
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
os.chdir(os.path.join(instance_dir, 'pages'))
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'no-title.md', tmpdir,
False)
assert os.path.exists(os.path.join(tmpdir, 'no-title.md'))
@ -44,7 +45,6 @@ def test_file_copy_symlink():
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
os.chdir(os.path.join(instance_dir, 'pages'))
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'symlink-to-foo.txt', tmpdir)
# need to copy the destination for os.path.exists to be happy with this
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'foo.txt', tmpdir)
@ -52,12 +52,44 @@ def test_file_copy_symlink():
assert os.path.islink(os.path.join(tmpdir, 'symlink-to-foo.txt'))
def test_file_copy_subdir_symlink():
"""Test the ability to sync a symlink in a subdirectory to the output dir."""
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
# need to make the subdirectory as if the generator already did
os.mkdir(os.path.join(tmpdir, 'subdir'))
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), 'subdir',
'relative-symlink-to-parent.md', tmpdir)
# need to copy the destination for os.path.exists to be happy with this
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'more-metadata.md', tmpdir)
logger.warning("created symlink %s",
os.readlink(os.path.join(tmpdir, 'subdir', 'relative-symlink-to-parent.md')))
assert os.path.islink(os.path.join(tmpdir, 'subdir', 'relative-symlink-to-parent.md'))
assert os.path.exists(os.path.join(tmpdir, 'subdir', 'relative-symlink-to-parent.md'))
def test_file_copy_symlink_of_markdown_also_has_html_symlink():
"""Test the ability to sync source and generated symlinks to the output dir."""
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'symlink-to-no-title.md', tmpdir,
True)
# need to copy the destination for os.path.exists to be happy with this
generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'no-title.md', tmpdir, True)
assert os.path.exists(os.path.join(tmpdir, 'symlink-to-no-title.md'))
assert os.path.islink(os.path.join(tmpdir, 'symlink-to-no-title.md'))
# a .md symlink should also create the .html symlink (see issue #24)
assert os.path.exists(os.path.join(tmpdir, 'symlink-to-no-title.html'))
assert os.path.islink(os.path.join(tmpdir, 'symlink-to-no-title.html'))
def test_dir_copy():
"""Test the ability to sync a directory to the output dir."""
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
os.chdir(os.path.join(instance_dir, 'pages'))
generator.build_subdir_in_destination(os.path.join(instance_dir, 'pages'), '', 'media', tmpdir)
assert os.path.exists(os.path.join(tmpdir, 'media'))
assert os.path.isdir(os.path.join(tmpdir, 'media'))
@ -68,7 +100,6 @@ def test_dir_copy_symlink():
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
os.chdir(os.path.join(instance_dir, 'pages'))
generator.build_subdir_in_destination(os.path.join(instance_dir, 'pages'), '', 'symlink-to-subdir', tmpdir)
# need to copy the destination for os.path.exists to be happy with this
generator.build_subdir_in_destination(os.path.join(instance_dir, 'pages'), '', 'subdir', tmpdir)
@ -88,3 +119,13 @@ def test_build_in_destination():
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'))
def test_build_in_destination_ignores_dot_files():
"""Test the ability to walk a source and populate the destination."""
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
generator = ssg.StaticSiteGenerator(src_dir, tmpdir)
generator.build_in_destination(os.path.join(src_dir, 'pages'), tmpdir)
assert not os.path.exists(os.path.join(tmpdir, '.ignored-file.md'))