from __future__ import absolute_import
from collections import Counter
import datetime
import os.path
import shutil
from .config import Config
from .exceptions import PostyError
from .page import Page
from .post import Post
from posty.renderer import (
HtmlRenderer,
JsonRenderer,
RssRenderer,
AtomRenderer,
Posty1RedirectRenderer,
)
from .util import slugify
[docs]
class Site(object):
"""
Representation of an entire site with posts and pages. This is the main
class that conrols everything.
:param site_path:
Path to the directory containing site content (pages, posts, templates)
:param config_path:
Path to the config file, defaults to ``$SITE_PATH/config.yml``
"""
def __init__(
self,
site_path: str = ".",
config_path: str | None = None,
config: Config | None = None,
) -> None:
self.site_path = site_path
if config:
self.config = config
else:
if config_path:
self.config_path = config_path
else:
self.config_path = os.path.join(site_path, "config.yml")
self.config = Config.from_yaml(self.config_path)
self.pages: list[Page] = []
self.posts: list[Post] = []
self.tags: list[str] = []
self.loaded = False
[docs]
def init(self) -> None:
"""
Initialize a new Posty site at the given path
"""
skel_path = os.path.join(os.path.dirname(__file__), "skel")
for thing in os.listdir(skel_path):
src = os.path.join(skel_path, thing)
dst = os.path.join(self.site_path, thing)
if os.path.exists(dst):
print("{} already exists, not overwriting".format(thing))
else:
if os.path.isdir(src):
shutil.copytree(src, dst)
elif os.path.isfile(src):
shutil.copy(src, dst)
[docs]
def load(self) -> None:
"""
Load the site from files on disk into our internal representation
"""
self._load_pages()
self._load_posts()
self.loaded = True
[docs]
def render(self, output_path: str = "build") -> None:
"""
Render the site with the various renderers
* HTML
* JSON
* RSS (if ``feeds.rss`` is True in the config)
* Atom (if ``feeds.atom`` is True in the config)
"""
HtmlRenderer(self, output_path=output_path).render_site()
JsonRenderer(self, output_path=output_path).render_site()
if self.config.feeds.rss:
RssRenderer(self, output_path=output_path).render_site()
if self.config.feeds.atom:
AtomRenderer(self, output_path=output_path).render_site()
if self.config.compat.redirect_posty1_urls:
Posty1RedirectRenderer(self, output_path=output_path).render_site()
def _load_pages(self) -> None:
pages = []
page_dir = os.path.join(self.site_path, "pages")
for filename in os.listdir(page_dir):
contents = open(os.path.join(page_dir, filename)).read()
pages.append(Page.from_yaml(contents, config=self.config))
self.pages = sorted(pages, key=lambda x: x.title.lower())
def _load_posts(self) -> None:
posts = []
tags = []
# Load each post
post_dir = os.path.join(self.site_path, "posts")
for filename in os.listdir(post_dir):
contents = open(os.path.join(post_dir, filename)).read()
post = Post.from_yaml(contents, config=self.config)
posts.append(post)
tags.extend(post.tags)
self.posts = sorted(posts, key=lambda x: x.date, reverse=True)
# uniquify tags and sort by frequency (descending)
self.tags = [t for t, c in Counter(tags).most_common()]
[docs]
def post(self, slug: str) -> Post:
"""
Returns a Post object by its slug
:param slug:
slug of the post to find
:returns:
A post dict
:raises PostyError:
if no post could be found
"""
for post in self.posts:
post_slug = post.slug or slugify(post.title)
if slug == post_slug:
return post
else:
raise PostyError(
"Unable to find post {}. Available posts: {}".format(
slug, [slugify(p.title) for p in self.pages]
)
)
[docs]
def page(self, slug: str) -> Page:
"""
Returns a Page object by its slug
:param slug:
slug of the page to find
:returns:
A page dict
:raises PostyError:
if no page could be found
"""
for page in self.pages:
page_slug = page.slug or slug == slugify(page.title)
if slug == page_slug:
return page
else:
raise PostyError(
"Unable to find post {}. Available posts: {}".format(
slug, [p.slug or slugify(p.title) for p in self.pages]
)
)
@property
def copyright(self) -> str:
"""
Returns a string of the copyright info, based on the configured author
and the years of the first and last post
"""
first_post = self.posts[-1]
last_post = self.posts[0]
copyright = "Copyright {start} - {end}, {author}".format(
author=self.config.author,
start=first_post.date.year,
end=last_post.date.year,
)
return copyright
[docs]
def new_post(self, name: str = "New Post") -> None:
"""
Create a new post in the site directory from the skeleton post
"""
post_dir = os.path.join(self.site_path, "posts")
if not os.path.exists(post_dir):
raise PostyError("You must initialize the site first")
date = datetime.date.today()
filename = "{}_{}.yaml".format(date, slugify(name))
post_path = os.path.join(post_dir, filename)
skel_path = os.path.join(
os.path.dirname(__file__), "skel/posts/1970-01-01_new-post.yaml"
)
post = Post.from_yaml(open(skel_path).read(), config=self.config)
post.title = name
post.date = date
with open(post_path, "w") as output_file:
output_file.write(post.to_yaml())
[docs]
def new_page(self, name: str = "New Page") -> None:
"""
Create a new page in the site directory from the skeleton page
"""
page_dir = os.path.join(self.site_path, "pages")
if not os.path.exists(page_dir):
raise PostyError("You must initialize the site first")
filename = "{}.yaml".format(slugify(name))
page_path = os.path.join(page_dir, filename)
skel_path = os.path.join(os.path.dirname(__file__), "skel/pages/new-page.yaml")
page = Page.from_yaml(open(skel_path).read(), config=self.config)
page.title = name
with open(page_path, "w") as output_file:
output_file.write(page.to_yaml())