1
0
Fork 0
forked from github/pelican
pelican-theme/pelican/generators.py

404 lines
16 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
from operator import attrgetter, itemgetter
from itertools import chain
from functools import partial
from datetime import datetime
from collections import defaultdict
import os
import math
import random
2011-07-27 01:35:06 +02:00
import urlparse
from jinja2 import Environment, FileSystemLoader, PrefixLoader, ChoiceLoader
from jinja2.exceptions import TemplateNotFound
from pelican.utils import copy, get_relative_path, process_translations, open
from pelican.utils import slugify
from pelican.contents import Article, Page, is_valid_content
from pelican.readers import read_file
from pelican.log import *
class Generator(object):
"""Baseclass generator"""
def __init__(self, *args, **kwargs):
for idx, item in enumerate(('context', 'settings', 'path', 'theme',
'output_path', 'markup')):
setattr(self, item, args[idx])
for arg, value in kwargs.items():
setattr(self, arg, value)
# templates cache
self._templates = {}
self._templates_path = os.path.expanduser(os.path.join(self.theme, 'templates'))
simple_loader = FileSystemLoader(os.path.join(os.path.dirname(os.path.abspath(__file__)), "themes", "simple", "templates"))
self._env = Environment(
loader=ChoiceLoader([
FileSystemLoader(self._templates_path),
simple_loader, # implicit inheritance
PrefixLoader({'!simple' : simple_loader}) # explicit inheritance
]),
extensions=self.settings.get('JINJA_EXTENSIONS', []),
)
debug('self._env.list_templates(): {0}'.format(self._env.list_templates()))
# get custom Jinja filters from user settings
custom_filters = self.settings.get('JINJA_FILTERS', {})
self._env.filters.update(custom_filters)
def get_template(self, name):
"""Return the template by name.
2010-11-20 02:25:42 +00:00
Use self.theme to get the templates to use, and return a list of
templates ready to use with Jinja2.
"""
if name not in self._templates:
try:
self._templates[name] = self._env.get_template(name + '.html')
except TemplateNotFound:
2010-11-20 02:25:42 +00:00
raise Exception('[templates] unable to load %s.html from %s' % (
name, self._templates_path))
return self._templates[name]
2010-11-20 02:25:42 +00:00
def get_files(self, path, exclude=[], extensions=None):
"""Return a list of files to use, based on rules
2010-10-30 20:17:23 +01:00
:param path: the path to search the file on
:param exclude: the list of path to exclude
"""
2010-11-20 02:25:42 +00:00
if not extensions:
extensions = self.markup
2010-10-30 20:17:23 +01:00
files = []
try:
iter = os.walk(path, followlinks=True)
except TypeError: # python 2.5 does not support followlinks
iter = os.walk(path)
for root, dirs, temp_files in iter:
2010-10-30 20:17:23 +01:00
for e in exclude:
if e in dirs:
dirs.remove(e)
files.extend([os.sep.join((root, f)) for f in temp_files
2010-11-20 02:25:42 +00:00
if True in [f.endswith(ext) for ext in extensions]])
2010-10-30 20:17:23 +01:00
return files
def _update_context(self, items):
"""Update the context with the given items from the currrent
processor.
"""
for item in items:
value = getattr(self, item)
if hasattr(value, 'items'):
value = value.items()
self.context[item] = value
class ArticlesGenerator(Generator):
"""Generate blog articles"""
def __init__(self, *args, **kwargs):
"""initialize properties"""
self.articles = [] # only articles in default language
self.translations = []
self.dates = {}
self.tags = defaultdict(list)
self.categories = defaultdict(list)
self.authors = defaultdict(list)
super(ArticlesGenerator, self).__init__(*args, **kwargs)
self.drafts = []
def generate_feeds(self, writer):
"""Generate the feeds from the current context, and output files."""
writer.write_feed(self.articles, self.context, self.settings['FEED'])
if 'FEED_RSS' in self.settings:
writer.write_feed(self.articles, self.context,
self.settings['FEED_RSS'], feed_type='rss')
for cat, arts in self.categories:
arts.sort(key=attrgetter('date'), reverse=True)
writer.write_feed(arts, self.context,
self.settings['CATEGORY_FEED'] % cat)
if 'CATEGORY_FEED_RSS' in self.settings:
writer.write_feed(arts, self.context,
self.settings['CATEGORY_FEED_RSS'] % cat,
feed_type='rss')
2010-12-18 20:42:15 +01:00
if 'TAG_FEED' in self.settings:
2010-12-17 09:25:19 +01:00
for tag, arts in self.tags.items():
arts.sort(key=attrgetter('date'), reverse=True)
writer.write_feed(arts, self.context,
self.settings['TAG_FEED'] % tag)
2010-12-17 09:25:19 +01:00
if 'TAG_FEED_RSS' in self.settings:
writer.write_feed(arts, self.context,
self.settings['TAG_FEED_RSS'] % tag, feed_type='rss')
translations_feeds = defaultdict(list)
2010-12-22 02:35:29 +03:00
for article in chain(self.articles, self.translations):
translations_feeds[article.lang].append(article)
for lang, items in translations_feeds.items():
items.sort(key=attrgetter('date'), reverse=True)
writer.write_feed(items, self.context,
self.settings['TRANSLATION_FEED'] % lang)
2010-12-17 09:25:19 +01:00
def generate_pages(self, writer):
"""Generate the pages on the disk"""
write = partial(
writer.write_file,
relative_urls = self.settings.get('RELATIVE_URLS')
)
# to minimize the number of relative path stuff modification
2011-02-15 13:48:57 +00:00
# in writer, articles pass first
article_template = self.get_template('article')
for article in chain(self.translations, self.articles):
2011-07-27 01:35:06 +02:00
add_to_url = u''
if 'ARTICLE_PERMALINK_STRUCTURE' in self.settings:
article_permalink_structure = self.settings['ARTICLE_PERMALINK_STRUCTURE']
article_permalink_structure = article_permalink_structure.lstrip('/')
2011-07-27 01:35:06 +02:00
# try to substitute any python datetime directive
add_to_url = article.date.strftime(article_permalink_structure)
# try to substitute any article metadata in rest file
add_to_url = add_to_url % article.__dict__
add_to_url = [slugify(i) for i in add_to_url.split('/')]
add_to_url = os.path.join(*add_to_url)
2011-07-27 01:35:06 +02:00
article.url = urlparse.urljoin(add_to_url, article.url)
article.save_as = urlparse.urljoin(add_to_url, article.save_as)
write(article.save_as,
article_template, self.context, article=article,
category=article.category)
PAGINATED_TEMPLATES = self.settings.get('PAGINATED_DIRECT_TEMPLATES')
for template in self.settings.get('DIRECT_TEMPLATES'):
2011-02-15 14:36:55 +01:00
paginated = {}
if template in PAGINATED_TEMPLATES:
2011-02-15 14:36:55 +01:00
paginated = {'articles': self.articles, 'dates': self.dates}
write('%s.html' % template, self.get_template(template), self.context,
blog=True, paginated=paginated, page_name=template)
# and subfolders after that
tag_template = self.get_template('tag')
2010-12-05 19:15:02 +00:00
for tag, articles in self.tags.items():
2011-06-12 22:18:50 +02:00
articles.sort(key=attrgetter('date'), reverse=True)
dates = [article for article in self.dates if article in articles]
write('tag/%s.html' % tag, tag_template, self.context, tag=tag,
articles=articles, dates=dates,
2011-02-15 14:36:55 +01:00
paginated={'articles': articles, 'dates': dates},
page_name='tag/%s' % tag)
category_template = self.get_template('category')
for cat, articles in self.categories:
dates = [article for article in self.dates if article in articles]
write('category/%s.html' % cat, category_template, self.context,
2011-02-15 14:36:55 +01:00
category=cat, articles=articles, dates=dates,
paginated={'articles': articles, 'dates': dates},
page_name='category/%s' % cat)
author_template = self.get_template('author')
for aut, articles in self.authors:
dates = [article for article in self.dates if article in articles]
write('author/%s.html' % aut, author_template, self.context,
author=aut, articles=articles, dates=dates,
paginated={'articles': articles, 'dates': dates},
page_name='author/%s' % aut)
for article in self.drafts:
write('drafts/%s.html' % article.slug, article_template, self.context,
article=article, category=article.category)
def generate_context(self):
"""change the context"""
# return the list of files to use
files = self.get_files(self.path, exclude=['pages',])
all_articles = []
for f in files:
2011-05-07 20:00:30 +01:00
content, metadata = read_file(f)
# if no category is set, use the name of the path as a category
2011-05-07 20:00:30 +01:00
if 'category' not in metadata.keys():
2011-03-27 12:46:33 +02:00
if os.path.dirname(f) == self.path:
category = self.settings['DEFAULT_CATEGORY']
2011-03-27 12:49:28 +02:00
else:
category = os.path.basename(os.path.dirname(f))
if category != '':
2011-05-07 20:00:30 +01:00
metadata['category'] = unicode(category)
2011-05-07 20:00:30 +01:00
if 'date' not in metadata.keys()\
and self.settings['FALLBACK_ON_FS_DATE']:
2011-05-07 20:00:30 +01:00
metadata['date'] = datetime.fromtimestamp(os.stat(f).st_ctime)
2011-05-07 20:00:30 +01:00
article = Article(content, metadata, settings=self.settings,
filename=f)
if not is_valid_content(article, f):
continue
if article.status == "published":
if hasattr(article, 'tags'):
for tag in article.tags:
self.tags[tag].append(article)
all_articles.append(article)
elif article.status == "draft":
self.drafts.append(article)
self.articles, self.translations = process_translations(all_articles)
for article in self.articles:
# only main articles are listed in categories, not translations
self.categories[article.category].append(article)
self.authors[article.author].append(article)
# sort the articles by date
self.articles.sort(key=attrgetter('date'), reverse=True)
self.dates = list(self.articles)
self.dates.sort(key=attrgetter('date'),
reverse=self.context['REVERSE_ARCHIVE_ORDER'])
# create tag cloud
tag_cloud = defaultdict(int)
for article in self.articles:
2011-03-23 16:41:24 +00:00
for tag in getattr(article, 'tags', []):
tag_cloud[tag] += 1
tag_cloud = sorted(tag_cloud.items(), key = itemgetter(1), reverse = True)
tag_cloud = tag_cloud[:self.settings.get('TAG_CLOUD_MAX_ITEMS')]
2011-03-27 12:46:33 +02:00
tags = map(itemgetter(1), tag_cloud)
2011-03-27 12:49:28 +02:00
if tags:
max_count = max(tags)
steps = self.settings.get('TAG_CLOUD_STEPS')
# calculate word sizes
self.tag_cloud = [
(
tag,
int(
2011-04-18 23:21:57 +08:00
math.floor(steps - (steps - 1) * math.log(count) / (math.log(max_count)or 1))
)
)
for tag, count in tag_cloud
]
# put words in chaos
random.shuffle(self.tag_cloud)
# and generate the output :)
# order the categories per name
self.categories = list(self.categories.items())
self.categories.sort(reverse=self.settings.get('REVERSE_CATEGORY_ORDER'))
self.authors = list(self.authors.items())
self.authors.sort()
self._update_context(('articles', 'dates', 'tags', 'categories', 'tag_cloud', 'authors'))
def generate_output(self, writer):
self.generate_feeds(writer)
self.generate_pages(writer)
class PagesGenerator(Generator):
"""Generate pages"""
def __init__(self, *args, **kwargs):
self.pages = []
super(PagesGenerator, self).__init__(*args, **kwargs)
def generate_context(self):
all_pages = []
for f in self.get_files(os.sep.join((self.path, 'pages'))):
2011-05-07 20:00:30 +01:00
content, metadata = read_file(f)
page = Page(content, metadata, settings=self.settings,
filename=f)
if not is_valid_content(page, f):
continue
all_pages.append(page)
self.pages, self.translations = process_translations(all_pages)
self._update_context(('pages', ))
2010-12-14 15:48:35 +00:00
self.context['PAGES'] = self.pages
def generate_output(self, writer):
for page in chain(self.translations, self.pages):
writer.write_file('pages/%s' % page.save_as, self.get_template('page'),
self.context, page=page,
relative_urls = self.settings.get('RELATIVE_URLS'))
class StaticGenerator(Generator):
2010-12-05 19:15:02 +00:00
"""copy static paths (what you want to cpy, like images, medias etc.
to output"""
def _copy_paths(self, paths, source, destination, output_path,
final_path=None):
"""Copy all the paths from source to destination"""
for path in paths:
copy(path, source, os.path.join(output_path, destination), final_path,
overwrite=True)
def generate_output(self, writer):
self._copy_paths(self.settings['STATIC_PATHS'], self.path,
'static', self.output_path)
2010-12-05 19:15:02 +00:00
self._copy_paths(self.settings['THEME_STATIC_PATHS'], self.theme,
'theme', self.output_path, '.')
# copy all the files needed
for source, destination in self.settings['FILES_TO_COPY']:
copy(source, self.path, self.output_path, destination, overwrite=True)
class PdfGenerator(Generator):
"""Generate PDFs on the output dir, for all articles and pages coming from
rst"""
def __init__(self, *args, **kwargs):
try:
from rst2pdf.createpdf import RstToPdf
self.pdfcreator = RstToPdf(breakside=0, stylesheets=['twelvepoint'])
except ImportError:
raise Exception("unable to find rst2pdf")
super(PdfGenerator, self).__init__(*args, **kwargs)
def _create_pdf(self, obj, output_path):
if obj.filename.endswith(".rst"):
filename = obj.slug + ".pdf"
output_pdf=os.path.join(output_path, filename)
# print "Generating pdf for", obj.filename, " in ", output_pdf
self.pdfcreator.createPdf(text=open(obj.filename), output=output_pdf)
info(u' [ok] writing %s' % output_pdf)
def generate_context(self):
pass
def generate_output(self, writer=None):
2011-02-15 13:48:57 +00:00
# we don't use the writer passed as argument here
# since we write our own files
info(u' Generating PDF files...')
pdf_path = os.path.join(self.output_path, 'pdf')
try:
os.mkdir(pdf_path)
except OSError:
error("Couldn't create the pdf output folder in " + pdf_path)
pass
for article in self.context['articles']:
self._create_pdf(article, pdf_path)
for page in self.context['pages']:
self._create_pdf(page, pdf_path)