Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
7205bb2aa5 | |||
88b678931e | |||
8c75947088 | |||
e75d5c48d2 | |||
201cd80804 | |||
a327c6b89c | |||
27d4d16572 | |||
b69bdb424a | |||
c46a1c0bae | |||
359916e7d9 | |||
d49b9d48a8 | |||
9caf08a277 | |||
8aabd93273 | |||
6d7987cfae | |||
abc05ee4e8 | |||
f623ffdd7c |
24
.reuse/dep5
24
.reuse/dep5
@ -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
|
|
40
CHANGELOG.md
40
CHANGELOG.md
@ -2,6 +2,46 @@
|
|||||||
|
|
||||||
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.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
|
## v2.0.0
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
28
README.md
28
README.md
@ -4,8 +4,7 @@ A lightweight static site generator for Markdown-based sites.
|
|||||||
|
|
||||||
## Installation and Usage
|
## Installation and Usage
|
||||||
|
|
||||||
I recommend getting a release from <https://git.incorporeal.org/bss/incorporeal-cms/releases> and
|
Something like the following should suffice:
|
||||||
installing the Python package in a virtualenv. Something like the following should suffice:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
% virtualenv --python=python3.9 env-py3.9
|
% 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
|
% 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
|
## Creating a Site
|
||||||
|
|
||||||
Put content, notably Markdown content, inside `./your-instance/pages/` and when you are ready, run the build command
|
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:
|
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. 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
|
* 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.
|
`/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,
|
* 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.
|
* All other files are copied directly, so images, text files, etc., can be referenced naturally as URLs.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@ -53,19 +52,18 @@ out and discuss issues and features and whatnot.
|
|||||||
|
|
||||||
## Author and Licensing
|
## 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
|
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General
|
||||||
it under the terms of the GNU General Public License as published by
|
Public License as published by the Free Software Foundation, either version 3 of the License, or (at your
|
||||||
the Free Software Foundation, version 3 of the License.
|
option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
more details.
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License along with this program. If not, see
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
### Content Output
|
### Content Output
|
||||||
|
|
||||||
|
28
REUSE.toml
Normal file
28
REUSE.toml
Normal 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"
|
@ -1,7 +1,7 @@
|
|||||||
"""An application for running my Markdown-based sites.
|
"""An application for running my Markdown-based sites.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
|
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 json
|
||||||
import logging
|
import logging
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Default configuration.
|
"""Default configuration.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
|
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
|
||||||
SPDX-License-Identifier: GPL-3.0-only
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Process the error page templates.
|
"""Process the error page templates.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
|
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 os
|
||||||
|
|
||||||
|
@ -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.
|
as well be pages/foo content.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2023 Brian S. Stephan <bss@incorporeal.org>
|
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 logging
|
||||||
import os
|
import os
|
||||||
@ -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!")
|
||||||
|
@ -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.
|
is our tweaks and customizations for pages my way.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
|
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 datetime
|
||||||
import logging
|
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))
|
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)
|
||||||
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,17 +70,25 @@ 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 = 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
|
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, mtime
|
||||||
|
|
||||||
|
|
||||||
def handle_markdown_file_path(path: str) -> str:
|
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."""
|
"""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)
|
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
|
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'
|
||||||
|
|
||||||
@ -90,16 +101,23 @@ def handle_markdown_file_path(path: str) -> str:
|
|||||||
return template.render(title=page_title,
|
return template.render(title=page_title,
|
||||||
config=Config,
|
config=Config,
|
||||||
description=get_meta_str(md, 'description'),
|
description=get_meta_str(md, 'description'),
|
||||||
image=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,14 +142,14 @@ 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
|
_ = Markup(md.convert(entry)) # nosec B704
|
||||||
page_name = (" ".join(md.Meta.get('title')) if md.Meta.get('title')
|
page_name = (" ".join(md.Meta.get('title')) if md.Meta.get('title')
|
||||||
else request_path_to_breadcrumb_display(request_path))
|
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:
|
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):
|
def request_path_to_breadcrumb_display(path):
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""Markdown extensions.
|
"""Markdown extensions.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2021 Brian S. Stephan <bss@incorporeal.org>
|
SPDX-FileCopyrightText: © 2021 Brian S. Stephan <bss@incorporeal.org>
|
||||||
SPDX-License-Identifier: GPL-3.0-only
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
"""
|
"""
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Create generic figures with captions.
|
"""Create generic figures with captions.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2022 Brian S. Stephan <bss@incorporeal.org>
|
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
|
import re
|
||||||
from xml.etree.ElementTree import SubElement # nosec B405
|
from xml.etree.ElementTree import SubElement # nosec B405
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Serve dot diagrams inline.
|
"""Serve dot diagrams inline.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2021 Brian S. Stephan <bss@incorporeal.org>
|
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 base64
|
||||||
import logging
|
import logging
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
"""Build an instance as a static site suitable for serving via e.g. Nginx.
|
"""Build an instance as a static site suitable for serving via e.g. Nginx.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
|
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 argparse
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
@ -16,6 +17,8 @@ from incorporealcms.error_pages import generate_error_pages
|
|||||||
from incorporealcms.feed import generate_feed
|
from incorporealcms.feed import generate_feed
|
||||||
from incorporealcms.markdown import handle_markdown_file_path
|
from incorporealcms.markdown import handle_markdown_file_path
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class StaticSiteGenerator(object):
|
class StaticSiteGenerator(object):
|
||||||
"""Generate static site output based on the instance's content."""
|
"""Generate static site output based on the instance's content."""
|
||||||
@ -53,10 +56,6 @@ class StaticSiteGenerator(object):
|
|||||||
pass
|
pass
|
||||||
self.build_in_destination(program_static_dir, static_output_dir, convert_markdown=False)
|
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
|
# generate the feeds
|
||||||
cprint("generating feeds", 'green')
|
cprint("generating feeds", 'green')
|
||||||
generate_feed('atom', self.instance_dir, tmp_output_dir)
|
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)
|
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)
|
||||||
# remove the absolute path of the directory from the base_dir
|
# 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
|
# create subdirs seen here for subsequent depth
|
||||||
for subdir in subdirs:
|
for subdir in subdirs:
|
||||||
self.build_subdir_in_destination(source_dir, base_dir, subdir, dest_dir)
|
self.build_subdir_in_destination(source_dir, base_dir, subdir, dest_dir)
|
||||||
|
|
||||||
# process and copy files
|
# process and copy files
|
||||||
for file_ in 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)
|
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:
|
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
|
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:
|
||||||
@ -133,21 +139,33 @@ 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:
|
||||||
|
# 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:
|
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)
|
||||||
|
|
||||||
# render markdown as HTML
|
# render markdown as HTML
|
||||||
if src.endswith('.md') and convert_markdown:
|
if src.endswith('.md') and convert_markdown:
|
||||||
rendered_file = dst.removesuffix('.md') + '.html'
|
rendered_file = dst.removesuffix('.md') + '.html'
|
||||||
|
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
|
||||||
@ -159,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
|
||||||
@ -167,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():
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
|
* 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 {
|
html {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
|
* 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 */
|
/* common styling via the base.css, used in light and dark */
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
|
* 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 */
|
/* common styling via the base.css, used in light and dark */
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
|
* 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 */
|
/* specify almost no styling, just fix some image and nav rendering */
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
* of my CMS, so I want to keep it around even in the static site.
|
* 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-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{#
|
{#
|
||||||
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
|
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" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{#
|
{#
|
||||||
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
|
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" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{#
|
{#
|
||||||
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
|
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" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{#
|
{#
|
||||||
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
|
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" %}
|
{% extends "base.html" %}
|
||||||
{% block site_class %}class="site-wrap site-wrap-double-width"{% endblock %}
|
{% block site_class %}class="site-wrap site-wrap-double-width"{% endblock %}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<!doctype html>{#
|
<!doctype html>{#
|
||||||
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
|
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">
|
<html lang="en">
|
||||||
<title>{{ title }}</title>
|
<title>{{ title }}</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
<meta property="og:url" content="{{ base_url }}">
|
||||||
{% if title %}<meta property="og:title" content="{{ title }}">{% endif %}
|
{% if title %}<meta property="og:title" content="{{ title }}">{% endif %}
|
||||||
{% if description %}<meta property="og:description" content="{{ description }}">{% 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 %}
|
{% if image %}<meta property="og:image" content="{{ image }}">{% endif %}
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
name = "incorporeal-cms"
|
name = "incorporeal-cms"
|
||||||
description = "A lightweight static site generator for Markdown-based sites."
|
description = "A lightweight static site generator for Markdown-based sites."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "GPL-3.0-only"}
|
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"},
|
||||||
]
|
]
|
||||||
@ -14,9 +14,8 @@ requires-python = ">=3.9"
|
|||||||
dependencies = ["feedgen", "jinja2", "Markdown", "termcolor"]
|
dependencies = ["feedgen", "jinja2", "Markdown", "termcolor"]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Framework :: Flask",
|
|
||||||
"Programming Language :: Python :: 3",
|
"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",
|
"Operating System :: OS Independent",
|
||||||
"Topic :: Text Processing :: Markup :: Markdown",
|
"Topic :: Text Processing :: Markup :: Markdown",
|
||||||
]
|
]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Test graphviz functionality.
|
"""Test graphviz functionality.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2021 Brian S. Stephan <bss@incorporeal.org>
|
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 os
|
||||||
import tempfile
|
import tempfile
|
||||||
@ -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,4 +57,29 @@ 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():
|
||||||
|
"""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
|
||||||
|
@ -18,7 +18,11 @@
|
|||||||
"level": "DEBUG",
|
"level": "DEBUG",
|
||||||
"handlers": ["console"]
|
"handlers": ["console"]
|
||||||
},
|
},
|
||||||
"incorporealcms.pages": {
|
"incorporealcms.markdown": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"handlers": ["console"]
|
||||||
|
},
|
||||||
|
"incorporealcms.ssg": {
|
||||||
"level": "DEBUG",
|
"level": "DEBUG",
|
||||||
"handlers": ["console"]
|
"handlers": ["console"]
|
||||||
}
|
}
|
||||||
|
@ -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'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
1
tests/instance/pages/.ignored-file.md
Normal file
1
tests/instance/pages/.ignored-file.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
this is ignored
|
@ -1,6 +1,6 @@
|
|||||||
Title: title for the page
|
Title: title for the page
|
||||||
Description: description of this page
|
Description: description of this page
|
||||||
made even longer
|
made even longer
|
||||||
Image: http://buh.com/test.img
|
Image: /test.img
|
||||||
|
|
||||||
hello
|
hello
|
||||||
|
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
|
@ -1,7 +1,7 @@
|
|||||||
"""Test command line invocations.
|
"""Test command line invocations.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
|
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 os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Test basic configuration stuff.
|
"""Test basic configuration stuff.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
|
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
|
import os
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Test the feed methods.
|
"""Test the feed methods.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2023 Brian S. Stephan <bss@incorporeal.org>
|
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 os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
@ -1,40 +1,45 @@
|
|||||||
"""Test the conversion of Markdown pages.
|
"""Test the conversion of Markdown pages.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
|
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 os
|
||||||
from unittest.mock import patch
|
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)
|
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_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', '/'), ('subdir', '/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', '/'), ('subdir', '/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')
|
||||||
@ -43,7 +48,7 @@ def test_generate_page_navs_subdir_with_title_parsing_real_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')
|
||||||
@ -53,26 +58,26 @@ def test_generate_page_navs_subdir_with_no_index():
|
|||||||
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 +86,21 @@ 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():
|
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."""
|
"""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):
|
with pytest.raises(NotImplementedError):
|
||||||
handle_markdown_file_path('redirect.md')
|
handle_markdown_file_path('redirect.md', os.path.join(INSTANCE_DIR, 'broken'))
|
||||||
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():
|
||||||
@ -131,21 +134,21 @@ def test_request_path_to_breadcrumb_display_patterns():
|
|||||||
|
|
||||||
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, 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'
|
||||||
|
|
||||||
|
|
||||||
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_path():
|
||||||
"""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 == '/subdir/'
|
||||||
assert page_title == '/subdir/ - example.org'
|
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():
|
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)
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
"""Test the high level SSG operations.
|
"""Test the high level SSG operations.
|
||||||
|
|
||||||
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
|
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 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,44 @@ 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():
|
||||||
|
"""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():
|
def test_dir_copy():
|
||||||
"""Test the ability to sync a directory to the output dir."""
|
"""Test the ability to sync a directory 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_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'))
|
||||||
@ -68,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)
|
||||||
@ -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, 'index.html'))
|
||||||
assert os.path.exists(os.path.join(tmpdir, 'subdir', 'index.md'))
|
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, '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'))
|
||||||
|
Loading…
Reference in New Issue
Block a user