merge with master

This commit is contained in:
Alexis Métaireau 2012-10-25 13:20:27 +02:00
commit 93c04cd79f
166 changed files with 4437 additions and 2285 deletions

View file

@ -8,10 +8,12 @@ import argparse
from pelican import signals
from pelican.generators import (ArticlesGenerator, PagesGenerator,
StaticGenerator, PdfGenerator, LessCSSGenerator)
StaticGenerator, PdfGenerator,
LessCSSGenerator, SourceFileGenerator)
from pelican.log import init
from pelican.settings import read_settings, _DEFAULT_CONFIG
from pelican.utils import clean_output_dir, files_changed, file_changed
from pelican.settings import read_settings
from pelican.utils import (clean_output_dir, files_changed, file_changed,
NoFilesError)
from pelican.writers import Writer
__major__ = 3
@ -23,55 +25,40 @@ logger = logging.getLogger(__name__)
class Pelican(object):
def __init__(self, settings=None, path=None, theme=None, output_path=None,
markup=None, delete_outputdir=False, plugin_path=None):
"""Read the settings, and performs some checks on the environment
before doing anything else.
def __init__(self, settings):
"""
Pelican initialisation, performs some checks on the environment before
doing anything else.
"""
if settings is None:
settings = _DEFAULT_CONFIG
self.path = path or settings['PATH']
if not self.path:
raise Exception('You need to specify a path containing the content'
' (see pelican --help for more information)')
if self.path.endswith('/'):
self.path = self.path[:-1]
# define the default settings
self.settings = settings
self._handle_deprecation()
self.theme = theme or settings['THEME']
output_path = output_path or settings['OUTPUT_PATH']
self.output_path = os.path.realpath(output_path)
self.markup = markup or settings['MARKUP']
self.delete_outputdir = delete_outputdir \
or settings['DELETE_OUTPUT_DIRECTORY']
# find the theme in pelican.theme if the given one does not exists
if not os.path.exists(self.theme):
theme_path = os.sep.join([os.path.dirname(
os.path.abspath(__file__)), "themes/%s" % self.theme])
if os.path.exists(theme_path):
self.theme = theme_path
else:
raise Exception("Impossible to find the theme %s" % theme)
self.path = settings['PATH']
self.theme = settings['THEME']
self.output_path = settings['OUTPUT_PATH']
self.markup = settings['MARKUP']
self.delete_outputdir = settings['DELETE_OUTPUT_DIRECTORY']
self.init_path()
self.init_plugins()
signals.initialized.send(self)
def init_path(self):
if not any(p in sys.path for p in ['', '.']):
logger.debug("Adding current directory to system path")
sys.path.insert(0, '')
def init_plugins(self):
self.plugins = self.settings['PLUGINS']
for plugin in self.plugins:
# if it's a string, then import it
if isinstance(plugin, basestring):
log.debug("Loading plugin `{0}' ...".format(plugin))
logger.debug("Loading plugin `{0}' ...".format(plugin))
plugin = __import__(plugin, globals(), locals(), 'module')
log.debug("Registering plugin `{0}' ...".format(plugin.__name__))
logger.debug("Registering plugin `{0}'".format(plugin.__name__))
plugin.register()
def _handle_deprecation(self):
@ -114,6 +101,35 @@ class Pelican(object):
self.settings[setting])
logger.warning("%s = '%s'" % (setting, self.settings[setting]))
if self.settings.get('FEED', False):
logger.warning('Found deprecated `FEED` in settings. Modify FEED'
' to FEED_ATOM in your settings and theme for the same behavior.'
' Temporarily setting FEED_ATOM for backwards compatibility.')
self.settings['FEED_ATOM'] = self.settings['FEED']
if self.settings.get('TAG_FEED', False):
logger.warning('Found deprecated `TAG_FEED` in settings. Modify '
' TAG_FEED to TAG_FEED_ATOM in your settings and theme for the '
'same behavior. Temporarily setting TAG_FEED_ATOM for backwards '
'compatibility.')
self.settings['TAG_FEED_ATOM'] = self.settings['TAG_FEED']
if self.settings.get('CATEGORY_FEED', False):
logger.warning('Found deprecated `CATEGORY_FEED` in settings. '
'Modify CATEGORY_FEED to CATEGORY_FEED_ATOM in your settings and '
'theme for the same behavior. Temporarily setting '
'CATEGORY_FEED_ATOM for backwards compatibility.')
self.settings['CATEGORY_FEED_ATOM'] =\
self.settings['CATEGORY_FEED']
if self.settings.get('TRANSLATION_FEED', False):
logger.warning('Found deprecated `TRANSLATION_FEED` in settings. '
'Modify TRANSLATION_FEED to TRANSLATION_FEED_ATOM in your '
'settings and theme for the same behavior. Temporarily setting '
'TRANSLATION_FEED_ATOM for backwards compatibility.')
self.settings['TRANSLATION_FEED_ATOM'] =\
self.settings['TRANSLATION_FEED']
def run(self):
"""Run the generators and return"""
@ -151,12 +167,28 @@ class Pelican(object):
if hasattr(p, 'generate_output'):
p.generate_output(writer)
signals.finalized.send(self)
def get_generator_classes(self):
generators = [StaticGenerator, ArticlesGenerator, PagesGenerator]
if self.settings['PDF_GENERATOR']:
generators.append(PdfGenerator)
if self.settings['LESS_GENERATOR']: # can be True or PATH to lessc
generators.append(LessCSSGenerator)
if self.settings['OUTPUT_SOURCES']:
generators.append(SourceFileGenerator)
for pair in signals.get_generators.send(self):
(funct, value) = pair
if not isinstance(value, (tuple, list)):
value = (value, )
for v in value:
if isinstance(v, type):
logger.debug('Found generator: {0}'.format(v))
generators.append(v)
return generators
def get_writer(self):
@ -213,11 +245,26 @@ def parse_arguments():
return parser.parse_args()
def get_instance(args):
markup = [a.strip().lower() for a in args.markup.split(',')]\
if args.markup else None
def get_config(args):
config = {}
if args.path:
config['PATH'] = os.path.abspath(os.path.expanduser(args.path))
if args.output:
config['OUTPUT_PATH'] = \
os.path.abspath(os.path.expanduser(args.output))
if args.markup:
config['MARKUP'] = [a.strip().lower() for a in args.markup.split(',')]
if args.theme:
abstheme = os.path.abspath(os.path.expanduser(args.theme))
config['THEME'] = abstheme if os.path.exists(abstheme) else args.theme
if args.delete_outputdir is not None:
config['DELETE_OUTPUT_DIRECTORY'] = args.delete_outputdir
return config
settings = read_settings(args.settings)
def get_instance(args):
settings = read_settings(args.settings, override=get_config(args))
cls = settings.get('PELICAN_CLASS')
if isinstance(cls, basestring):
@ -225,19 +272,17 @@ def get_instance(args):
module = __import__(module)
cls = getattr(module, cls_name)
return cls(settings, args.path, args.theme, args.output, markup,
args.delete_outputdir)
return cls(settings)
def main():
args = parse_arguments()
init(args.verbosity)
# Split the markup languages only if some have been given. Otherwise,
# populate the variable with None.
pelican = get_instance(args)
try:
if args.autoreload:
files_found_error = True
while True:
try:
# Check source dir for changed files ending with the given
@ -247,6 +292,8 @@ def main():
# have.
if files_changed(pelican.path, pelican.markup) or \
files_changed(pelican.theme, ['']):
if not files_found_error:
files_found_error = True
pelican.run()
# reload also if settings.py changed
@ -258,7 +305,19 @@ def main():
time.sleep(.5) # sleep to avoid cpu load
except KeyboardInterrupt:
logger.warning("Keyboard interrupt, quitting.")
break
except NoFilesError:
if files_found_error:
logger.warning("No valid files found in content. "
"Nothing to generate.")
files_found_error = False
time.sleep(1) # sleep to avoid cpu load
except Exception, e:
logger.warning(
"Caught exception \"{}\". Reloading.".format(e)
)
continue
else:
pelican.run()
except Exception, e:

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import copy
import locale
import logging
import functools
@ -10,7 +11,7 @@ from sys import platform, stdin
from pelican.settings import _DEFAULT_CONFIG
from pelican.utils import slugify, truncate_html_words
from pelican import signals
logger = logging.getLogger(__name__)
@ -21,6 +22,7 @@ class Page(object):
:param content: the string to parse, containing the original content.
"""
mandatory_properties = ('title',)
default_template = 'page'
def __init__(self, content, metadata=None, settings=None,
filename=None):
@ -28,7 +30,7 @@ class Page(object):
if not metadata:
metadata = {}
if not settings:
settings = _DEFAULT_CONFIG
settings = copy.deepcopy(_DEFAULT_CONFIG)
self.settings = settings
self._content = content
@ -44,6 +46,9 @@ class Page(object):
# also keep track of the metadata attributes available
self.metadata = local_metadata
#default template if it's not defined in page
self.template = self._get_template()
# default author to the one in settings if not defined
if not hasattr(self, 'author'):
if 'AUTHOR' in settings:
@ -101,6 +106,8 @@ class Page(object):
if 'summary' in metadata:
self._summary = metadata['summary']
signals.content_object_init.send(self.__class__, instance=self)
def check_properties(self):
"""test that each mandatory property is set."""
for prop in self.mandatory_properties:
@ -153,9 +160,16 @@ class Page(object):
url = property(functools.partial(get_url_setting, key='url'))
save_as = property(functools.partial(get_url_setting, key='save_as'))
def _get_template(self):
if hasattr(self, 'template') and self.template is not None:
return self.template
else:
return self.default_template
class Article(Page):
mandatory_properties = ('title', 'date', 'category')
default_template = 'article'
class Quote(Page):

View file

@ -6,6 +6,7 @@ import logging
import datetime
import subprocess
from codecs import open
from collections import defaultdict
from functools import partial
from itertools import chain
@ -16,7 +17,7 @@ from jinja2.exceptions import TemplateNotFound
from pelican.contents import Article, Page, Category, is_valid_content
from pelican.readers import read_file
from pelican.utils import copy, process_translations, open
from pelican.utils import copy, process_translations
from pelican import signals
@ -36,8 +37,11 @@ class Generator(object):
# templates cache
self._templates = {}
self._templates_path = os.path.expanduser(
os.path.join(self.theme, 'templates'))
self._templates_path = []
self._templates_path.append(os.path.expanduser(
os.path.join(self.theme, 'templates')))
self._templates_path += self.settings.get('EXTRA_TEMPLATES_PATHS', [])
theme_path = os.path.dirname(os.path.abspath(__file__))
@ -116,6 +120,7 @@ class ArticlesGenerator(Generator):
self.dates = {}
self.tags = defaultdict(list)
self.categories = defaultdict(list)
self.related_posts = []
self.authors = defaultdict(list)
super(ArticlesGenerator, self).__init__(*args, **kwargs)
self.drafts = []
@ -123,10 +128,17 @@ class ArticlesGenerator(Generator):
def generate_feeds(self, writer):
"""Generate the feeds from the current context, and output files."""
if self.settings.get('FEED_ATOM') is None \
and self.settings.get('FEED_RSS') is None:
return
elif self.settings.get('SITEURL') is '':
logger.warning(
'Feeds generated without SITEURL set properly may not be valid'
)
if self.settings.get('FEED'):
if self.settings.get('FEED_ATOM'):
writer.write_feed(self.articles, self.context,
self.settings['FEED'])
self.settings['FEED_ATOM'])
if self.settings.get('FEED_RSS'):
writer.write_feed(self.articles, self.context,
@ -134,44 +146,49 @@ class ArticlesGenerator(Generator):
for cat, arts in self.categories:
arts.sort(key=attrgetter('date'), reverse=True)
if self.settings.get('CATEGORY_FEED'):
if self.settings.get('CATEGORY_FEED_ATOM'):
writer.write_feed(arts, self.context,
self.settings['CATEGORY_FEED'] % cat)
self.settings['CATEGORY_FEED_ATOM'] % cat)
if self.settings.get('CATEGORY_FEED_RSS'):
writer.write_feed(arts, self.context,
self.settings['CATEGORY_FEED_RSS'] % cat,
feed_type='rss')
if self.settings.get('TAG_FEED') or self.settings.get('TAG_FEED_RSS'):
if self.settings.get('TAG_FEED_ATOM') \
or self.settings.get('TAG_FEED_RSS'):
for tag, arts in self.tags.items():
arts.sort(key=attrgetter('date'), reverse=True)
if self.settings.get('TAG_FEED'):
if self.settings.get('TAG_FEED_ATOM'):
writer.write_feed(arts, self.context,
self.settings['TAG_FEED'] % tag)
self.settings['TAG_FEED_ATOM'] % tag)
if self.settings.get('TAG_FEED_RSS'):
writer.write_feed(arts, self.context,
self.settings['TAG_FEED_RSS'] % tag,
feed_type='rss')
if self.settings.get('TRANSLATION_FEED'):
if self.settings.get('TRANSLATION_FEED_ATOM') or \
self.settings.get('TRANSLATION_FEED_RSS'):
translations_feeds = defaultdict(list)
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)
if self.settings.get('TRANSLATION_FEED_ATOM'):
writer.write_feed(items, self.context,
self.settings['TRANSLATION_FEED_ATOM'] % lang)
if self.settings.get('TRANSLATION_FEED_RSS'):
writer.write_feed(items, self.context,
self.settings['TRANSLATION_FEED_RSS'] % lang,
feed_type='rss')
def generate_articles(self, write):
"""Generate the articles."""
article_template = self.get_template('article')
for article in chain(self.translations, self.articles):
write(article.save_as,
article_template, self.context, article=article,
category=article.category)
write(article.save_as, self.get_template(article.template),
self.context, article=article, category=article.category)
def generate_direct_templates(self, write):
"""Generate direct templates pages"""
@ -183,7 +200,7 @@ class ArticlesGenerator(Generator):
save_as = self.settings.get("%s_SAVE_AS" % template.upper(),
'%s.html' % template)
if not save_as:
continue
continue
write(save_as, self.get_template(template),
self.context, blog=True, paginated=paginated,
@ -222,10 +239,10 @@ class ArticlesGenerator(Generator):
def generate_drafts(self, write):
"""Generate drafts pages."""
article_template = self.get_template('article')
for article in self.drafts:
write('drafts/%s.html' % article.slug, article_template,
self.context, article=article, category=article.category)
write('drafts/%s.html' % article.slug,
self.get_template(article.template), self.context,
article=article, category=article.category)
def generate_pages(self, writer):
"""Generate the pages on the disk"""
@ -264,7 +281,7 @@ class ArticlesGenerator(Generator):
if 'category' not in metadata:
if os.path.dirname(f) == article_path: # if the article is not in a subdirectory
category = self.settings['DEFAULT_CATEGORY']
category = self.settings['DEFAULT_CATEGORY']
else:
category = os.path.basename(os.path.dirname(f))\
.decode('utf-8')
@ -272,9 +289,13 @@ class ArticlesGenerator(Generator):
if category != '':
metadata['category'] = Category(category, self.settings)
if 'date' not in metadata and self.settings['FALLBACK_ON_FS_DATE']:
if 'date' not in metadata and self.settings['DEFAULT_DATE']:
if self.settings['DEFAULT_DATE'] == 'fs':
metadata['date'] = datetime.datetime.fromtimestamp(
os.stat(f).st_ctime)
os.stat(f).st_ctime)
else:
metadata['date'] = datetime.datetime(
*self.settings['DEFAULT_DATE'])
signals.article_generate_context.send(self, metadata=metadata)
article = Article(content, metadata, settings=self.settings,
@ -305,7 +326,7 @@ class ArticlesGenerator(Generator):
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'])
reverse=self.context['NEWEST_FIRST_ARCHIVES'])
# create tag cloud
tag_cloud = defaultdict(int)
@ -345,7 +366,9 @@ class ArticlesGenerator(Generator):
self.authors.sort(key=lambda item: item[0].name)
self._update_context(('articles', 'dates', 'tags', 'categories',
'tag_cloud', 'authors'))
'tag_cloud', 'authors', 'related_posts'))
signals.article_generator_finalized.send(self)
def generate_output(self, writer):
self.generate_feeds(writer)
@ -357,32 +380,46 @@ class PagesGenerator(Generator):
def __init__(self, *args, **kwargs):
self.pages = []
self.hidden_pages = []
self.hidden_translations = []
super(PagesGenerator, self).__init__(*args, **kwargs)
signals.pages_generator_init.send(self)
def generate_context(self):
all_pages = []
hidden_pages = []
for f in self.get_files(
os.path.join(self.path, self.settings['PAGE_DIR']),
exclude=self.settings['PAGE_EXCLUDES']):
try:
content, metadata = read_file(f)
content, metadata = read_file(f, settings=self.settings)
except Exception, e:
logger.error(u'Could not process %s\n%s' % (f, str(e)))
logger.warning(u'Could not process %s\n%s' % (f, str(e)))
continue
signals.pages_generate_context.send(self, metadata=metadata )
page = Page(content, metadata, settings=self.settings,
filename=f)
if not is_valid_content(page, f):
continue
all_pages.append(page)
if page.status == "published":
all_pages.append(page)
elif page.status == "hidden":
hidden_pages.append(page)
else:
logger.warning(u"Unknown status %s for file %s, skipping it." %
(repr(unicode.encode(page.status, 'utf-8')),
repr(f)))
self.pages, self.translations = process_translations(all_pages)
self.hidden_pages, self.hidden_translations = process_translations(hidden_pages)
self._update_context(('pages', ))
self.context['PAGES'] = self.pages
def generate_output(self, writer):
for page in chain(self.translations, self.pages):
writer.write_file(page.save_as, self.get_template('page'),
for page in chain(self.translations, self.pages,
self.hidden_translations, self.hidden_pages):
writer.write_file(page.save_as, self.get_template(page.template),
self.context, page=page,
relative_urls=self.settings.get('RELATIVE_URLS'))
@ -406,7 +443,23 @@ class StaticGenerator(Generator):
# Define the assets environment that will be passed to the
# generators. The StaticGenerator must then be run first to have
# the assets in the output_path before generating the templates.
assets_url = self.settings['SITEURL'] + '/theme/'
# Let ASSET_URL honor Pelican's RELATIVE_URLS setting.
# Hint for templates:
# Current version of webassets seem to remove any relative
# paths at the beginning of the URL. So, if RELATIVE_URLS
# is on, ASSET_URL will start with 'theme/', regardless if we
# set assets_url here to './theme/' or to 'theme/'.
# XXX However, this breaks the ASSET_URL if user navigates to
# a sub-URL, e.g. if he clicks on a category. To workaround this
# issue, I use
# <link rel="stylesheet" href="{{ SITEURL }}/{{ ASSET_URL }}">
# instead of
# <link rel="stylesheet" href="{{ ASSET_URL }}">
if self.settings.get('RELATIVE_URLS'):
assets_url = './theme/'
else:
assets_url = self.settings['SITEURL'] + '/theme/'
assets_src = os.path.join(self.output_path, 'theme')
self.assets_env = AssetsEnvironment(assets_src, assets_url)
@ -430,13 +483,20 @@ class PdfGenerator(Generator):
"""Generate PDFs on the output dir, for all articles and pages coming from
rst"""
def __init__(self, *args, **kwargs):
super(PdfGenerator, self).__init__(*args, **kwargs)
try:
from rst2pdf.createpdf import RstToPdf
pdf_style_path = os.path.join(self.settings['PDF_STYLE_PATH']) \
if 'PDF_STYLE_PATH' in self.settings.keys() \
else ''
pdf_style = self.settings['PDF_STYLE'] if 'PDF_STYLE' \
in self.settings.keys() \
else 'twelvepoint'
self.pdfcreator = RstToPdf(breakside=0,
stylesheets=['twelvepoint'])
stylesheets=[pdf_style],
style_path=[pdf_style_path])
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"):
@ -444,7 +504,7 @@ class PdfGenerator(Generator):
output_pdf = os.path.join(output_path, filename)
# print "Generating pdf for", obj.filename, " in ", output_pdf
with open(obj.filename) as f:
self.pdfcreator.createPdf(text=f, output=output_pdf)
self.pdfcreator.createPdf(text=f.read(), output=output_pdf)
logger.info(u' [ok] writing %s' % output_pdf)
def generate_context(self):
@ -468,6 +528,19 @@ class PdfGenerator(Generator):
for page in self.context['pages']:
self._create_pdf(page, pdf_path)
class SourceFileGenerator(Generator):
def generate_context(self):
self.output_extension = self.settings['OUTPUT_SOURCES_EXTENSION']
def _create_source(self, obj, output_path):
filename = os.path.splitext(obj.save_as)[0]
dest = os.path.join(output_path, filename + self.output_extension)
copy('', obj.filename, dest)
def generate_output(self, writer=None):
logger.info(u' Generating source files...')
for object in chain(self.context['articles'], self.context['pages']):
self._create_source(object, self.output_path)
class LessCSSGenerator(Generator):
"""Compile less css files."""

View file

@ -4,13 +4,14 @@ from pelican import signals
License plugin for Pelican
==========================
Simply add license variable in article's context, which contain
the license text.
This plugin allows you to define a LICENSE setting and adds the contents of that
license variable to the article's context, making that variable available to use
from within your theme's templates.
Settings:
---------
Add LICENSE to your settings file to define default license.
Define LICENSE in your settings file with the contents of your default license.
"""

View file

@ -5,20 +5,22 @@ from pelican import signals
Gravatar plugin for Pelican
===========================
Simply add author_gravatar variable in article's context, which contains
the gravatar url.
This plugin assigns the ``author_gravatar`` variable to the Gravatar URL and
makes the variable available within the article's context.
Settings:
---------
Add AUTHOR_EMAIL to your settings file to define default author email.
Add AUTHOR_EMAIL to your settings file to define the default author's email
address. Obviously, that email address must be associated with a Gravatar
account.
Article metadata:
------------------
:email: article's author email
If one of them are defined, the author_gravatar variable is added to
If one of them are defined, the author_gravatar variable is added to the
article's context.
"""

View file

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) FELD Boris <lothiraldan@gmail.com>
Multiple part support
=====================
Create a navigation menu for multi-part related_posts
Article metadata:
------------------
:parts: a unique identifier for multi-part posts, must be the same in each
post part.
Usage
-----
{% if article.metadata.parts_articles %}
<ol>
{% for part_article in article.metadata.parts_articles %}
{% if part_article == article %}
<li>
<a href='{{ SITEURL }}/{{ part_article.url }}'><b>{{ part_article.title }}</b>
</a>
</li>
{% else %}
<li>
<a href='{{ SITEURL }}/{{ part_article.url }}'>{{ part_article.title }}
</a>
</li>
{% endif %}
{% endfor %}
</ol>
{% endif %}
"""
from collections import defaultdict
from pelican import signals
def aggregate_multi_part(generator):
multi_part = defaultdict(list)
for article in generator.articles:
if 'parts' in article.metadata:
multi_part[article.metadata['parts']].append(article)
for part_id in multi_part:
parts = multi_part[part_id]
# Sort by date
parts.sort(key=lambda x: x.metadata['date'])
for article in parts:
article.metadata['parts_articles'] = parts
def register():
signals.article_generator_finalized.connect(aggregate_multi_part)

View file

@ -0,0 +1,52 @@
from pelican import signals
"""
Related posts plugin for Pelican
================================
Adds related_posts variable to article's context
Settings
--------
To enable, add
from pelican.plugins import related_posts
PLUGINS = [related_posts]
to your settings.py.
Usage
-----
{% if article.related_posts %}
<ul>
{% for related_post in article.related_posts %}
<li>{{ related_post }}</li>
{% endfor %}
</ul>
{% endif %}
"""
related_posts = []
def add_related_posts(generator, metadata):
if 'tags' in metadata:
for tag in metadata['tags']:
#print tag
for related_article in generator.tags[tag]:
related_posts.append(related_article)
if len(related_posts) < 1:
return
relation_score = dict(zip(set(related_posts), map(related_posts.count,
set(related_posts))))
ranked_related = sorted(relation_score, key=relation_score.get)
metadata["related_posts"] = ranked_related[:5]
def register():
signals.article_generate_context.connect(add_related_posts)

190
pelican/plugins/sitemap.py Normal file
View file

@ -0,0 +1,190 @@
import collections
import os.path
from datetime import datetime
from logging import warning, info
from codecs import open
from pelican import signals, contents
TXT_HEADER = u"""{0}/index.html
{0}/archives.html
{0}/tags.html
{0}/categories.html
"""
XML_HEADER = u"""<?xml version="1.0" encoding="utf-8"?>
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
"""
XML_URL = u"""
<url>
<loc>{0}/{1}</loc>
<lastmod>{2}</lastmod>
<changefreq>{3}</changefreq>
<priority>{4}</priority>
</url>
"""
XML_FOOTER = u"""
</urlset>
"""
def format_date(date):
if date.tzinfo:
tz = date.strftime('%s')
tz = tz[:-2] + ':' + tz[-2:]
else:
tz = "-00:00"
return date.strftime("%Y-%m-%dT%H:%M:%S") + tz
class SitemapGenerator(object):
def __init__(self, context, settings, path, theme, output_path, *null):
self.output_path = output_path
self.context = context
self.now = datetime.now()
self.siteurl = settings.get('SITEURL')
self.format = 'xml'
self.changefreqs = {
'articles': 'monthly',
'indexes': 'daily',
'pages': 'monthly'
}
self.priorities = {
'articles': 0.5,
'indexes': 0.5,
'pages': 0.5
}
config = settings.get('SITEMAP', {})
if not isinstance(config, dict):
warning("sitemap plugin: the SITEMAP setting must be a dict")
else:
fmt = config.get('format')
pris = config.get('priorities')
chfreqs = config.get('changefreqs')
if fmt not in ('xml', 'txt'):
warning("sitemap plugin: SITEMAP['format'] must be `txt' or `xml'")
warning("sitemap plugin: Setting SITEMAP['format'] on `xml'")
elif fmt == 'txt':
self.format = fmt
return
valid_keys = ('articles', 'indexes', 'pages')
valid_chfreqs = ('always', 'hourly', 'daily', 'weekly', 'monthly',
'yearly', 'never')
if isinstance(pris, dict):
for k, v in pris.iteritems():
if k in valid_keys and not isinstance(v, (int, float)):
default = self.priorities[k]
warning("sitemap plugin: priorities must be numbers")
warning("sitemap plugin: setting SITEMAP['priorities']"
"['{0}'] on {1}".format(k, default))
pris[k] = default
self.priorities.update(pris)
elif pris is not None:
warning("sitemap plugin: SITEMAP['priorities'] must be a dict")
warning("sitemap plugin: using the default values")
if isinstance(chfreqs, dict):
for k, v in chfreqs.iteritems():
if k in valid_keys and v not in valid_chfreqs:
default = self.changefreqs[k]
warning("sitemap plugin: invalid changefreq `{0}'".format(v))
warning("sitemap plugin: setting SITEMAP['changefreqs']"
"['{0}'] on '{1}'".format(k, default))
chfreqs[k] = default
self.changefreqs.update(chfreqs)
elif chfreqs is not None:
warning("sitemap plugin: SITEMAP['changefreqs'] must be a dict")
warning("sitemap plugin: using the default values")
def write_url(self, page, fd):
if getattr(page, 'status', 'published') != 'published':
return
page_path = os.path.join(self.output_path, page.url)
if not os.path.exists(page_path):
return
lastmod = format_date(getattr(page, 'date', self.now))
if isinstance(page, contents.Article):
pri = self.priorities['articles']
chfreq = self.changefreqs['articles']
elif isinstance(page, contents.Page):
pri = self.priorities['pages']
chfreq = self.changefreqs['pages']
else:
pri = self.priorities['indexes']
chfreq = self.changefreqs['indexes']
if self.format == 'xml':
fd.write(XML_URL.format(self.siteurl, page.url, lastmod, chfreq, pri))
else:
fd.write(self.siteurl + '/' + loc + '\n')
def generate_output(self, writer):
path = os.path.join(self.output_path, 'sitemap.{0}'.format(self.format))
pages = self.context['pages'] + self.context['articles'] \
+ [ c for (c, a) in self.context['categories']] \
+ [ t for (t, a) in self.context['tags']] \
+ [ a for (a, b) in self.context['authors']]
for article in self.context['articles']:
pages += article.translations
info('writing {0}'.format(path))
with open(path, 'w', encoding='utf-8') as fd:
if self.format == 'xml':
fd.write(XML_HEADER)
else:
fd.write(TXT_HEADER.format(self.siteurl))
FakePage = collections.namedtuple('FakePage',
['status',
'date',
'url'])
for standard_page_url in ['index.html',
'archives.html',
'tags.html',
'categories.html']:
fake = FakePage(status='published',
date=self.now,
url=standard_page_url)
self.write_url(fake, fd)
for page in pages:
self.write_url(page, fd)
if self.format == 'xml':
fd.write(XML_FOOTER)
def get_generators(generators):
return SitemapGenerator
def register():
signals.get_generators.connect(get_generators)

View file

@ -16,7 +16,7 @@ except ImportError:
import re
from pelican.contents import Category, Tag, Author
from pelican.utils import get_date, open
from pelican.utils import get_date, pelican_open
_METADATA_PROCESSORS = {
@ -63,6 +63,18 @@ def render_node_to_html(document, node):
return visitor.astext()
class PelicanHTMLTranslator(HTMLTranslator):
def visit_abbreviation(self, node):
attrs = {}
if node.hasattr('explanation'):
attrs['title'] = node['explanation']
self.body.append(self.starttag(node, 'abbr', '', **attrs))
def depart_abbreviation(self, node):
self.body.append('</abbr>')
class RstReader(Reader):
enabled = bool(docutils)
file_extensions = ['rst']
@ -90,8 +102,9 @@ class RstReader(Reader):
def _get_publisher(self, filename):
extra_params = {'initial_header_level': '2'}
pub = docutils.core.Publisher(
destination_class=docutils.io.StringOutput)
destination_class=docutils.io.StringOutput)
pub.set_components('standalone', 'restructuredtext', 'html')
pub.writer.translator_class = PelicanHTMLTranslator
pub.process_programmatic_settings(None, extra_params, None)
pub.set_source(source_path=filename)
pub.publish()
@ -116,8 +129,13 @@ class MarkdownReader(Reader):
def read(self, filename):
"""Parse content and metadata of markdown files"""
text = open(filename)
md = Markdown(extensions=set(self.extensions + ['meta']))
markdown_extensions = self.settings.get('MARKDOWN_EXTENSIONS', [])
if isinstance(markdown_extensions, (str, unicode)):
markdown_extensions = [m.strip() for m in
markdown_extensions.split(',')]
text = pelican_open(filename)
md = Markdown(extensions=set(
self.extensions + markdown_extensions + ['meta']))
content = md.convert(text)
metadata = {}
@ -133,7 +151,7 @@ class HtmlReader(Reader):
def read(self, filename):
"""Parse content and metadata of (x)HTML files"""
with open(filename) as content:
with pelican_open(filename) as content:
metadata = {'title': 'unnamed'}
for i in self._re.findall(content):
key = i.split(':')[0][5:].strip()
@ -172,8 +190,8 @@ def read_file(filename, fmt=None, settings=None):
# eventually filter the content with typogrify if asked so
if settings and settings['TYPOGRIFY']:
from typogrify import Typogrify
content = Typogrify.typogrify(content)
metadata['title'] = Typogrify.typogrify(metadata['title'])
from typogrify.filters import typogrify
content = typogrify(content)
metadata['title'] = typogrify(metadata['title'])
return content, metadata

View file

@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
from docutils import nodes
from docutils.parsers.rst import directives, Directive
from docutils import nodes, utils
from docutils.parsers.rst import directives, roles, Directive
from pygments.formatters import HtmlFormatter
from pygments import highlight
from pygments.lexers import get_lexer_by_name, TextLexer
import re
INLINESTYLES = False
DEFAULT = HtmlFormatter(noclasses=INLINESTYLES)
@ -94,3 +95,18 @@ class YouTube(Directive):
nodes.raw('', '</div>', format='html')]
directives.register_directive('youtube', YouTube)
_abbr_re = re.compile('\((.*)\)$')
class abbreviation(nodes.Inline, nodes.TextElement): pass
def abbr_role(typ, rawtext, text, lineno, inliner, options={}, content=[]):
text = utils.unescape(text)
m = _abbr_re.search(text)
if m is None:
return [abbreviation(text, text)], []
abbr = text[:m.start()].strip()
expl = m.group(1)
return [abbreviation(abbr, abbr, explanation=expl)], []
roles.register_local_role('abbr', abbr_role)

View file

@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
import copy
import imp
import inspect
import os
import locale
import logging
@ -21,18 +24,21 @@ _DEFAULT_CONFIG = {'PATH': '.',
'MARKUP': ('rst', 'md'),
'STATIC_PATHS': ['images', ],
'THEME_STATIC_PATHS': ['static', ],
'FEED': 'feeds/all.atom.xml',
'CATEGORY_FEED': 'feeds/%s.atom.xml',
'TRANSLATION_FEED': 'feeds/all-%s.atom.xml',
'FEED_ATOM': 'feeds/all.atom.xml',
'CATEGORY_FEED_ATOM': 'feeds/%s.atom.xml',
'TRANSLATION_FEED_ATOM': 'feeds/all-%s.atom.xml',
'FEED_MAX_ITEMS': '',
'SITEURL': '',
'SITENAME': 'A Pelican Blog',
'DISPLAY_PAGES_ON_MENU': True,
'PDF_GENERATOR': False,
'OUTPUT_SOURCES': False,
'OUTPUT_SOURCES_EXTENSION': '.text',
'DEFAULT_CATEGORY': 'misc',
'FALLBACK_ON_FS_DATE': True,
'DEFAULT_DATE': 'fs',
'WITH_FUTURE_DATES': True,
'CSS_FILE': 'main.css',
'REVERSE_ARCHIVE_ORDER': False,
'NEWEST_FIRST_ARCHIVES': True,
'REVERSE_CATEGORY_ORDER': False,
'DELETE_OUTPUT_DIRECTORY': False,
'ARTICLE_URL': '{slug}.html',
@ -54,6 +60,7 @@ _DEFAULT_CONFIG = {'PATH': '.',
'TAG_CLOUD_STEPS': 4,
'TAG_CLOUD_MAX_ITEMS': 100,
'DIRECT_TEMPLATES': ('index', 'tags', 'categories', 'archives'),
'EXTRA_TEMPLATES_PATHS' : [],
'PAGINATED_DIRECT_TEMPLATES': ('index', ),
'PELICAN_CLASS': 'pelican.Pelican',
'DEFAULT_DATE_FORMAT': '%a %d %B %Y',
@ -71,46 +78,70 @@ _DEFAULT_CONFIG = {'PATH': '.',
'SUMMARY_MAX_LENGTH': 50,
'WEBASSETS': False,
'PLUGINS': [],
'MARKDOWN_EXTENSIONS': ['toc', ],
}
def read_settings(filename=None):
def read_settings(filename=None, override=None):
if filename:
local_settings = get_settings_from_file(filename)
# Make the paths relative to the settings file
for p in ['PATH', 'OUTPUT_PATH', 'THEME']:
if p in local_settings and local_settings[p] is not None \
and not isabs(local_settings[p]):
absp = os.path.abspath(os.path.normpath(os.path.join(
os.path.dirname(filename), local_settings[p])))
if p != 'THEME' or os.path.exists(p):
local_settings[p] = absp
else:
local_settings = _DEFAULT_CONFIG
configured_settings = configure_settings(local_settings, None, filename)
return configured_settings
local_settings = copy.deepcopy(_DEFAULT_CONFIG)
if override:
local_settings.update(override)
return configure_settings(local_settings)
def get_settings_from_file(filename, default_settings=None):
"""Load a Python file into a dictionary.
def get_settings_from_module(module=None, default_settings=_DEFAULT_CONFIG):
"""
if default_settings == None:
default_settings = _DEFAULT_CONFIG
context = default_settings.copy()
if filename:
tempdict = {}
execfile(filename, tempdict)
for key in tempdict:
if key.isupper():
context[key] = tempdict[key]
Load settings from a module, returning a dict.
"""
context = copy.deepcopy(default_settings)
if module is not None:
context.update(
(k, v) for k, v in inspect.getmembers(module) if k.isupper())
return context
def configure_settings(settings, default_settings=None, filename=None):
"""Provide optimizations, error checking, and warnings for loaded settings"""
if default_settings is None:
default_settings = _DEFAULT_CONFIG
def get_settings_from_file(filename, default_settings=_DEFAULT_CONFIG):
"""
Load settings from a file path, returning a dict.
# Make the paths relative to the settings file
if filename:
for path in ['PATH', 'OUTPUT_PATH']:
if path in settings:
if settings[path] is not None and not isabs(settings[path]):
settings[path] = os.path.abspath(os.path.normpath(
os.path.join(os.path.dirname(filename), settings[path]))
)
"""
name = os.path.basename(filename).rpartition(".")[0]
module = imp.load_source(name, filename)
return get_settings_from_module(module, default_settings=default_settings)
def configure_settings(settings):
"""
Provide optimizations, error checking, and warnings for loaded settings
"""
if not 'PATH' in settings or not os.path.isdir(settings['PATH']):
raise Exception('You need to specify a path containing the content'
' (see pelican --help for more information)')
# find the theme in pelican.theme if the given one does not exists
if not os.path.isdir(settings['THEME']):
theme_path = os.sep.join([os.path.dirname(
os.path.abspath(__file__)), "themes/%s" % settings['THEME']])
if os.path.exists(theme_path):
settings['THEME'] = theme_path
else:
raise Exception("Impossible to find the theme %s"
% settings['THEME'])
# if locales is not a list, make it one
locales = settings['LOCALE']
@ -125,7 +156,7 @@ def configure_settings(settings, default_settings=None, filename=None):
for locale_ in locales:
try:
locale.setlocale(locale.LC_ALL, locale_)
break # break if it is successfull
break # break if it is successful
except locale.Error:
pass
else:
@ -142,7 +173,7 @@ def configure_settings(settings, default_settings=None, filename=None):
settings['FEED_DOMAIN'] = settings['SITEURL']
# Warn if feeds are generated with both SITEURL & FEED_DOMAIN undefined
if (('FEED' in settings) or ('FEED_RSS' in settings)) and (not 'FEED_DOMAIN' in settings):
if (('FEED_ATOM' in settings) or ('FEED_RSS' in settings)) and (not 'FEED_DOMAIN' in settings):
logger.warn("Since feed URLs should always be absolute, you should specify "
"FEED_DOMAIN in your settings. (e.g., 'FEED_DOMAIN = "
"http://www.example.com')")
@ -161,4 +192,11 @@ def configure_settings(settings, default_settings=None, filename=None):
logger.warn("You must install the webassets module to use WEBASSETS.")
settings['WEBASSETS'] = False
if 'OUTPUT_SOURCES_EXTENSION' in settings:
if not isinstance(settings['OUTPUT_SOURCES_EXTENSION'], str):
settings['OUTPUT_SOURCES_EXTENSION'] = _DEFAULT_CONFIG['OUTPUT_SOURCES_EXTENSION']
logger.warn("Detected misconfiguration with OUTPUT_SOURCES_EXTENSION."
" falling back to the default extension " +
_DEFAULT_CONFIG['OUTPUT_SOURCES_EXTENSION'])
return settings

View file

@ -1,5 +1,11 @@
from blinker import signal
initialized = signal('pelican_initialized')
finalized = signal('pelican_finalized')
article_generate_context = signal('article_generate_context')
article_generator_init = signal('article_generator_init')
article_generator_finalized = signal('article_generate_finalized')
get_generators = signal('get_generators')
pages_generate_context = signal('pages_generate_context')
pages_generator_init = signal('pages_generator_init')
content_object_init = signal('content_object_init')

View file

@ -70,9 +70,6 @@ p {margin-bottom: 1.143em;}
strong, b {font-weight: bold;}
em, i {font-style: italic;}
::-moz-selection {background: #F6CF74; color: #fff;}
::selection {background: #F6CF74; color: #fff;}
/* Lists */
ul {
list-style: outside disc;
@ -100,7 +97,7 @@ dl {margin: 0 0 1.5em 0;}
dt {font-weight: bold;}
dd {margin-left: 1.5em;}
pre{background-color: #000; padding: 10px; color: #fff; margin: 10px; overflow: auto;}
pre{background-color: rgb(238, 238, 238); padding: 10px; margin: 10px; overflow: auto;}
/* Quotes */
blockquote {
@ -144,8 +141,8 @@ aside, nav, article, figure {
/***** Layout *****/
.body {clear: both; margin: 0 auto; width: 800px;}
img.right figure.right {float: right; margin: 0 0 2em 2em;}
img.left, figure.left {float: right; margin: 0 0 2em 2em;}
img.right, figure.right {float: right; margin: 0 0 2em 2em;}
img.left, figure.left {float: left; margin: 0 2em 2em 0;}
/*
Header
@ -163,7 +160,6 @@ img.left, figure.left {float: right; margin: 0 0 2em 2em;}
font-weight: bold;
margin: 0 0 .6em .2em;
text-decoration: none;
width: 427px;
}
#banner h1 a:hover, #banner h1 a:active {
background: none;
@ -312,7 +308,8 @@ img.left, figure.left {float: right; margin: 0 0 2em 2em;}
.social a[type$='atom+xml'], .social a[type$='rss+xml'] {background-image: url('../images/icons/rss.png');}
.social a[href*='twitter.com'] {background-image: url('../images/icons/twitter.png');}
.social a[href*='linkedin.com'] {background-image: url('../images/icons/linkedin.png');}
.social a[href*='gitorious.org'] {background-image: url('../images/icons/gitorious.org');}
.social a[href*='gitorious.org'] {background-image: url('../images/icons/gitorious.png');}
.social a[href*='gittip.com'] {background-image: url('../images/icons/gittip.png');}
/*
About

View file

@ -1,5 +1,5 @@
.hll {
background-color:#FFFFCC;
background-color:#eee;
}
.c {
color:#408090;

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

View file

@ -1,11 +1,12 @@
{% if GOOGLE_ANALYTICS %}
<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '{{GOOGLE_ANALYTICS}}']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
<script type="text/javascript">
try {
var pageTracker = _gat._getTracker("{{GOOGLE_ANALYTICS}}");
pageTracker._trackPageview();
} catch(err) {}</script>
{% endif %}

View file

@ -4,7 +4,9 @@
<title>{% block title %}{{ SITENAME }}{%endblock%}</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="{{ SITEURL }}/theme/css/{{ CSS_FILE }}" type="text/css" />
<link href="{{ FEED_DOMAIN }}/{{ FEED }}" type="application/atom+xml" rel="alternate" title="{{ SITENAME }} Atom Feed" />
{% if FEED_ATOM %}
<link href="{{ FEED_DOMAIN }}/{{ FEED_ATOM }}" type="application/atom+xml" rel="alternate" title="{{ SITENAME }} Atom Feed" />
{% endif %}
{% if FEED_RSS %}
<link href="{{ FEED_DOMAIN }}/{{ FEED_RSS }}" type="application/rss+xml" rel="alternate" title="{{ SITENAME }} RSS Feed" />
{% endif %}
@ -56,7 +58,7 @@
<div class="social">
<h2>social</h2>
<ul>
<li><a href="{{ FEED_DOMAIN }}/{{ FEED }}" type="application/atom+xml" rel="alternate">atom feed</a></li>
<li><a href="{{ FEED_DOMAIN }}/{{ FEED_ATOM }}" type="application/atom+xml" rel="alternate">atom feed</a></li>
{% if FEED_RSS %}
<li><a href="{{ FEED_DOMAIN }}/{{ FEED_RSS }}" type="application/rss+xml" rel="alternate">rss feed</a></li>
{% endif %}
@ -71,7 +73,7 @@
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="http://pelican.notmyidea.org/">Pelican</a>, which takes great advantage of <a href="http://python.org">Python</a>.
Proudly powered by <a href="http://getpelican.com/">Pelican</a>, which takes great advantage of <a href="http://python.org">Python</a>.
</address><!-- /#about -->
<p>The theme is by <a href="http://coding.smashingmagazine.com/2009/08/04/designing-a-html-5-layout-from-scratch/">Smashing Magazine</a>, thanks!</p>

View file

@ -4,6 +4,24 @@
{% block head %}
<title>{% block title %}{{ SITENAME }}{% endblock title %}</title>
<meta charset="utf-8" />
{% if FEED_ATOM %}
<link href="{{ FEED_DOMAIN }}/{{ FEED_ATOM }}" type="application/atom+xml" rel="alternate" title="{{ SITENAME }} Atom Feed" />
{% endif %}
{% if FEED_RSS %}
<link href="{{ FEED_DOMAIN }}/{{ FEED_RSS }}" type="application/rss+xml" rel="alternate" title="{{ SITENAME }} RSS Feed" />
{% endif %}
{% if CATEGORY_FEED_ATOM %}
<link href="{{ FEED_DOMAIN }}/{{ CATEGORY_FEED_ATOM|format(category) }}" type="application/atom+xml" rel="alternate" title="{{ SITENAME }} Categories Atom Feed" />
{% endif %}
{% if CATEGORY_FEED_RSS %}
<link href="{{ FEED_DOMAIN }}/{{ CATEGORY_FEED_RSS|format(category) }}" type="application/rss+xml" rel="alternate" title="{{ SITENAME }} Categories RSS Feed" />
{% endif %}
{% if TAG_FEED_ATOM %}
<link href="{{ FEED_DOMAIN }}/{{ TAG_FEED_ATOM|format(tag) }}" type="application/atom+xml" rel="alternate" title="{{ SITENAME }} Tags Atom Feed" />
{% endif %}
{% if TAG_FEED_RSS %}
<link href="{{ FEED_DOMAIN }}/{{ TAG_FEED_RSS|format(tag) }}" type="application/rss+xml" rel="alternate" title="{{ SITENAME }} Tags RSS Feed" />
{% endif %}
{% endblock head %}
</head>
@ -29,7 +47,7 @@
{% endblock %}
<footer id="contentinfo" class="body">
<address id="about" class="vcard body">
Proudly powered by <a href="http://pelican.notmyidea.org/">Pelican</a>,
Proudly powered by <a href="http://getpelican.com/">Pelican</a>,
which takes great advantage of <a href="http://python.org">Python</a>.
</address><!-- /#about -->
</footer><!-- /#contentinfo -->

View file

@ -184,6 +184,8 @@ def build_header(title, date, author, categories, tags):
header = '%s\n%s\n' % (title, '#' * len(title))
if date:
header += ':date: %s\n' % date
if author:
header += ':author: %s\n' % author
if categories:
header += ':category: %s\n' % ', '.join(categories)
if tags:
@ -196,6 +198,8 @@ def build_markdown_header(title, date, author, categories, tags):
header = 'Title: %s\n' % title
if date:
header += 'Date: %s\n' % date
if author:
header += 'Author: %s\n' % author
if categories:
header += 'Category: %s\n' % ', '.join(categories)
if tags:
@ -216,7 +220,7 @@ def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=Fals
filename = os.path.basename(filename)
# option to put files in directories with categories names
if dircat and (len(categories) == 1):
if dircat and (len(categories) > 0):
catname = slugify(categories[0])
out_filename = os.path.join(output_path, catname, filename+ext)
if not os.path.isdir(os.path.join(output_path, catname)):
@ -233,7 +237,7 @@ def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=Fals
# Replace newlines with paragraphs wrapped with <p> so
# HTML is valid before conversion
paragraphs = content.split('\n\n')
paragraphs = [u'<p>{}</p>'.format(p) for p in paragraphs]
paragraphs = [u'<p>{0}</p>'.format(p) for p in paragraphs]
new_content = ''.join(paragraphs)
fp.write(new_content)

View file

@ -18,11 +18,13 @@ CONF = {
'ftp_host': 'localhost',
'ftp_user': 'anonymous',
'ftp_target_dir': '/',
'ssh_host': 'locahost',
'ssh_host': 'localhost',
'ssh_port': 22,
'ssh_user': 'root',
'ssh_target_dir': '/var/www',
'dropbox_dir' : '~/Dropbox/Public/',
'default_pagination' : 10,
'siteurl': '',
'lang': 'en'
}
@ -88,7 +90,7 @@ def ask(question, answer=str, default=None, l=None):
r = default
break
else:
print("You must answer `yes' or `no'")
print("You must answer 'yes' or 'no'")
return r
elif answer == int:
r = None
@ -111,12 +113,12 @@ def ask(question, answer=str, default=None, l=None):
print('You must enter an integer')
return r
else:
raise NotImplemented('Arguent `answer` must be str, bool or integer')
raise NotImplemented('Argument `answer` must be str, bool, or integer')
def main():
parser = argparse.ArgumentParser(
description="A kickstarter for pelican",
description="A kickstarter for Pelican",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-p', '--path', default=".",
help="The path to generate the blog into")
@ -125,7 +127,7 @@ def main():
parser.add_argument('-a', '--author', metavar="author",
help='Set the author name of the website')
parser.add_argument('-l', '--lang', metavar="lang",
help='Set the default lang of the website')
help='Set the default web site language')
args = parser.parse_args()
@ -137,36 +139,45 @@ Please answer the following questions so this script can generate the files need
'''.format(v=__version__))
CONF['basedir'] = os.path.abspath(ask('Where do you want to create your new Web site ?', answer=str, default=args.path))
CONF['sitename'] = ask('What will be the title of this Web site ?', answer=str, default=args.title)
CONF['author'] = ask('Who will be the author of this Web site ?', answer=str, default=args.author)
CONF['lang'] = ask('What will be the default language of this Web site ?', str, args.lang or CONF['lang'], 2)
project = os.path.join(os.environ.get('VIRTUAL_ENV', '.'), '.project')
if os.path.isfile(project):
CONF['basedir'] = open(project, 'r').read().rstrip("\n")
print('Using project associated with current virtual environment. Will save to:\n%s\n' % CONF['basedir'])
else:
CONF['basedir'] = os.path.abspath(ask('Where do you want to create your new web site?', answer=str, default=args.path))
CONF['with_pagination'] = ask('Do you want to enable article pagination ?', bool, bool(CONF['default_pagination']))
CONF['sitename'] = ask('What will be the title of this web site?', answer=str, default=args.title)
CONF['author'] = ask('Who will be the author of this web site?', answer=str, default=args.author)
CONF['lang'] = ask('What will be the default language of this web site?', str, args.lang or CONF['lang'], 2)
if ask('Do you want to specify a URL prefix? e.g., http://example.com ', answer=bool, default=True):
CONF['siteurl'] = ask('What is your URL prefix? (see above example; no trailing slash)', str, CONF['siteurl'])
CONF['with_pagination'] = ask('Do you want to enable article pagination?', bool, bool(CONF['default_pagination']))
if CONF['with_pagination']:
CONF['default_pagination'] = ask('So how many articles per page do you want ?', int, CONF['default_pagination'])
CONF['default_pagination'] = ask('How many articles per page do you want?', int, CONF['default_pagination'])
else:
CONF['default_pagination'] = False
mkfile = ask('Do you want to generate a Makefile to easily manage your website ?', bool, True)
mkfile = ask('Do you want to generate a Makefile to easily manage your website?', bool, True)
develop = ask('Do you want an auto-reload & simpleHTTP script to assist with theme and site development?', bool, True)
if mkfile:
if ask('Do you want to upload your website using FTP ?', answer=bool, default=False):
CONF['ftp_host'] = ask('What is the hostname of your FTP server ?', str, CONF['ftp_host'])
CONF['ftp_user'] = ask('What is your username on this server ?', str, CONF['ftp_user'])
CONF['ftp_target_dir'] = ask('Where do you want to put your website on this server ?', str, CONF['ftp_target_dir'])
if ask('Do you want to upload your website using SSH ?', answer=bool, default=False):
CONF['ssh_host'] = ask('What is the hostname of your SSH server ?', str, CONF['ssh_host'])
CONF['ssh_user'] = ask('What is your username on this server ?', str, CONF['ssh_user'])
CONF['ssh_target_dir'] = ask('Where do you want to put your website on this server ?', str, CONF['ssh_target_dir'])
if ask('Do you want to upload your website using Dropbox ?', answer=bool, default=False):
CONF['dropbox_dir'] = ask('Where is your Dropbox directory ?', str, CONF['dropbox_dir'])
if ask('Do you want to upload your website using FTP?', answer=bool, default=False):
CONF['ftp_host'] = ask('What is the hostname of your FTP server?', str, CONF['ftp_host'])
CONF['ftp_user'] = ask('What is your username on that server?', str, CONF['ftp_user'])
CONF['ftp_target_dir'] = ask('Where do you want to put your web site on that server?', str, CONF['ftp_target_dir'])
if ask('Do you want to upload your website using SSH?', answer=bool, default=False):
CONF['ssh_host'] = ask('What is the hostname of your SSH server?', str, CONF['ssh_host'])
CONF['ssh_port'] = ask('What is the port of your SSH server?', int, CONF['ssh_port'])
CONF['ssh_user'] = ask('What is your username on that server?', str, CONF['ssh_user'])
CONF['ssh_target_dir'] = ask('Where do you want to put your web site on that server?', str, CONF['ssh_target_dir'])
if ask('Do you want to upload your website using Dropbox?', answer=bool, default=False):
CONF['dropbox_dir'] = ask('Where is your Dropbox directory?', str, CONF['dropbox_dir'])
try:
os.makedirs(os.path.join(CONF['basedir'], 'src'))
os.makedirs(os.path.join(CONF['basedir'], 'content'))
except OSError, e:
print('Error: {0}'.format(e))
@ -176,8 +187,20 @@ Please answer the following questions so this script can generate the files need
print('Error: {0}'.format(e))
try:
with open(os.path.join(CONF['basedir'], 'pelican.conf.py'), 'w') as fd:
for line in get_template('pelican.conf.py'):
with open(os.path.join(CONF['basedir'], 'pelicanconf.py'), 'w') as fd:
conf_python = dict()
for key, value in CONF.iteritems():
conf_python[key] = repr(value)
for line in get_template('pelicanconf.py'):
template = string.Template(line)
fd.write(template.safe_substitute(conf_python))
fd.close()
except OSError, e:
print('Error: {0}'.format(e))
try:
with open(os.path.join(CONF['basedir'], 'publishconf.py'), 'w') as fd:
for line in get_template('publishconf.py'):
template = string.Template(line)
fd.write(template.safe_substitute(CONF))
fd.close()
@ -185,7 +208,6 @@ Please answer the following questions so this script can generate the files need
print('Error: {0}'.format(e))
if mkfile:
try:
with open(os.path.join(CONF['basedir'], 'Makefile'), 'w') as fd:
for line in get_template('Makefile'):
@ -195,4 +217,20 @@ Please answer the following questions so this script can generate the files need
except OSError, e:
print('Error: {0}'.format(e))
if develop:
conf_shell = dict()
for key, value in CONF.iteritems():
if isinstance(value, basestring) and ' ' in value:
value = '"' + value.replace('"', '\\"') + '"'
conf_shell[key] = value
try:
with open(os.path.join(CONF['basedir'], 'develop_server.sh'), 'w') as fd:
for line in get_template('develop_server.sh'):
template = string.Template(line)
fd.write(template.safe_substitute(conf_shell))
fd.close()
os.chmod((os.path.join(CONF['basedir'], 'develop_server.sh')), 0755)
except OSError, e:
print('Error: {0}'.format(e))
print('Done. Your new project is available at %s' % CONF['basedir'])

View file

@ -48,9 +48,11 @@ def main():
parser.add_argument('-i', '--install', dest='to_install', nargs='+', metavar="theme path",
help='The themes to install ')
help='The themes to install')
parser.add_argument('-r', '--remove', dest='to_remove', nargs='+', metavar="theme name",
help='The themes to remove')
parser.add_argument('-U', '--upgrade', dest='to_upgrade', nargs='+',
metavar="theme path", help='The themes to upgrade')
parser.add_argument('-s', '--symlink', dest='to_symlink', nargs='+', metavar="theme path",
help="Same as `--install', but create a symbolic link instead of copying the theme. Useful for theme development")
parser.add_argument('-c', '--clean', dest='clean', action="store_true",
@ -62,6 +64,9 @@ def main():
args = parser.parse_args()
to_install = args.to_install or args.to_upgrade
to_sym = args.to_symlink or args.clean
if args.action:
@ -69,8 +74,7 @@ def main():
list_themes(args.verbose)
elif args.action is 'path':
print(_THEMES_PATH)
elif args.to_install or args.to_remove or args.to_symlink or args.clean:
elif to_install or args.to_remove or to_sym:
if args.to_remove:
if args.verbose:
print('Removing themes...')
@ -85,6 +89,13 @@ def main():
for i in args.to_install:
install(i, v=args.verbose)
if args.to_upgrade:
if args.verbose:
print('Upgrading themes...')
for i in args.to_upgrade:
install(i, v=args.verbose, u=True)
if args.to_symlink:
if args.verbose:
print('Linking themes...')
@ -149,22 +160,38 @@ def remove(theme_name, v=False):
err(target + ' : no such file or directory')
def install(path, v=False):
def install(path, v=False, u=False):
"""Installs a theme"""
if not os.path.exists(path):
err(path + ' : no such file or directory')
elif not os.path.isdir(path):
err(path + ' : no a directory')
err(path + ' : not a directory')
else:
theme_name = os.path.basename(os.path.normpath(path))
theme_path = os.path.join(_THEMES_PATH, theme_name)
if os.path.exists(theme_path):
exists = os.path.exists(theme_path)
if exists and not u:
err(path + ' : already exists')
elif exists and u:
remove(theme_name, v)
install(path, v)
else:
if v:
print("Copying `{p}' to `{t}' ...".format(p=path, t=theme_path))
try:
shutil.copytree(path, theme_path)
try:
if os.name == 'posix':
for root, dirs, files in os.walk(theme_path):
for d in dirs:
dname = os.path.join(root, d)
os.chmod(dname, 0755)
for f in files:
fname = os.path.join(root, f)
os.chmod(fname, 0644)
except OSError, e:
err("Cannot change permissions of files or directory in `{r}':\n{e}".format(r=theme_path, e=str(e)), die=False)
except Exception, e:
err("Cannot copy `{p}' to `{t}':\n{e}".format(p=path, t=theme_path, e=str(e)))
@ -174,7 +201,7 @@ def symlink(path, v=False):
if not os.path.exists(path):
err(path + ' : no such file or directory')
elif not os.path.isdir(path):
err(path + ' : no a directory')
err(path + ' : not a directory')
else:
theme_name = os.path.basename(os.path.normpath(path))
theme_path = os.path.join(_THEMES_PATH, theme_name)

View file

@ -1,16 +1,18 @@
PELICAN=$pelican
PELICANOPTS=$pelicanopts
BASEDIR=$$(PWD)
INPUTDIR=$$(BASEDIR)/src
BASEDIR=$$(CURDIR)
INPUTDIR=$$(BASEDIR)/content
OUTPUTDIR=$$(BASEDIR)/output
CONFFILE=$$(BASEDIR)/pelican.conf.py
CONFFILE=$$(BASEDIR)/pelicanconf.py
PUBLISHCONF=$$(BASEDIR)/publishconf.py
FTP_HOST=$ftp_host
FTP_USER=$ftp_user
FTP_TARGET_DIR=$ftp_target_dir
SSH_HOST=$ssh_host
SSH_PORT=$ssh_port
SSH_USER=$ssh_user
SSH_TARGET_DIR=$ssh_target_dir
@ -22,10 +24,15 @@ help:
@echo 'Usage: '
@echo ' make html (re)generate the web site '
@echo ' make clean remove the generated files '
@echo ' ftp_upload upload the web site using FTP '
@echo ' ssh_upload upload the web site using SSH '
@echo ' dropbox_upload upload the web site using Dropbox '
@echo ' rsync_upload upload the web site using rsync/ssh'
@echo ' make regenerate regenerate files upon modification '
@echo ' make publish generate using production settings '
@echo ' make serve serve site at http://localhost:8000'
@echo ' make devserver start/restart develop_server.sh '
@echo ' ssh_upload upload the web site via SSH '
@echo ' rsync_upload upload the web site via rsync+ssh '
@echo ' dropbox_upload upload the web site via Dropbox '
@echo ' ftp_upload upload the web site via FTP '
@echo ' github upload the web site via gh-pages '
@echo ' '
@ -36,23 +43,34 @@ $$(OUTPUTDIR)/%.html:
$$(PELICAN) $$(INPUTDIR) -o $$(OUTPUTDIR) -s $$(CONFFILE) $$(PELICANOPTS)
clean:
rm -fr $$(OUTPUTDIR)
mkdir $$(OUTPUTDIR)
find $$(OUTPUTDIR) -mindepth 1 -delete
dropbox_upload: $$(OUTPUTDIR)/index.html
regenerate: clean
$$(PELICAN) -r $$(INPUTDIR) -o $$(OUTPUTDIR) -s $$(CONFFILE) $$(PELICANOPTS)
serve:
cd $$(OUTPUTDIR) && python -m SimpleHTTPServer
devserver:
$$(BASEDIR)/develop_server.sh restart
publish:
$$(PELICAN) $$(INPUTDIR) -o $$(OUTPUTDIR) -s $$(PUBLISHCONF) $$(PELICANOPTS)
ssh_upload: publish
scp -P $$(SSH_PORT) -r $$(OUTPUTDIR)/* $$(SSH_USER)@$$(SSH_HOST):$$(SSH_TARGET_DIR)
rsync_upload: publish
rsync -e "ssh -p $(SSH_PORT)" -P -rvz --delete $(OUTPUTDIR) $(SSH_USER)@$(SSH_HOST):$(SSH_TARGET_DIR)
dropbox_upload: publish
cp -r $$(OUTPUTDIR)/* $$(DROPBOX_DIR)
ssh_upload: $$(OUTPUTDIR)/index.html
scp -r $$(OUTPUTDIR)/* $$(SSH_USER)@$$(SSH_HOST):$$(SSH_TARGET_DIR)
rsync_upload: $$(OUTPUTDIR)/index.html
rsync -e ssh -P -rvz --delete $(OUTPUTDIR)/* $(SSH_USER)@$(SSH_HOST):$(SSH_TARGET_DIR)
ftp_upload: $$(OUTPUTDIR)/index.html
ftp_upload: publish
lftp ftp://$$(FTP_USER)@$$(FTP_HOST) -e "mirror -R $$(OUTPUTDIR) $$(FTP_TARGET_DIR) ; quit"
github: $$(OUTPUTDIR)/index.html
github: publish
ghp-import $$(OUTPUTDIR)
git push origin gh-pages
.PHONY: html help clean ftp_upload ssh_upload rsync_upload dropbox_upload github
.PHONY: html help clean regenerate serve devserver publish ssh_upload rsync_upload dropbox_upload ftp_upload github

View file

@ -0,0 +1,84 @@
#!/usr/bin/env bash
##
# This section should match your Makefile
##
PELICAN=$pelican
PELICANOPTS=$pelicanopts
BASEDIR=$$(pwd)
INPUTDIR=$$BASEDIR/content
OUTPUTDIR=$$BASEDIR/output
CONFFILE=$$BASEDIR/pelicanconf.py
###
# Don't change stuff below here unless you are sure
###
SRV_PID=$$BASEDIR/srv.pid
PELICAN_PID=$$BASEDIR/pelican.pid
function usage(){
echo "usage: $$0 (stop) (start) (restart)"
echo "This starts pelican in debug and reload mode and then launches"
echo "A SimpleHTTP server to help site development. It doesn't read"
echo "your pelican options so you edit any paths in your Makefile"
echo "you will need to edit it as well"
exit 3
}
function shut_down(){
if [[ -f $$SRV_PID ]]; then
PID=$$(cat $$SRV_PID)
PROCESS=$$(ps -p $$PID | tail -n 1 | awk '{print $$4}')
if [[ $$PROCESS == python ]]; then
echo "Killing SimpleHTTPServer"
kill $$PID
else
echo "Stale PID, deleting"
fi
rm $$SRV_PID
else
echo "SimpleHTTPServer PIDFile not found"
fi
if [[ -f $$PELICAN_PID ]]; then
PID=$$(cat $$PELICAN_PID)
PROCESS=$$(ps -p $$PID | tail -n 1 | awk '{print $$4}')
if [[ $$PROCESS != "" ]]; then
echo "Killing Pelican"
kill $$PID
else
echo "Stale PID, deleting"
fi
rm $$PELICAN_PID
else
echo "Pelican PIDFile not found"
fi
}
function start_up(){
echo "Starting up Pelican and SimpleHTTPServer"
shift
$$PELICAN --debug --autoreload -r $$INPUTDIR -o $$OUTPUTDIR -s $$CONFFILE $$PELICANOPTS &
echo $$! > $$PELICAN_PID
cd $$OUTPUTDIR
python -m SimpleHTTPServer &
echo $$! > $$SRV_PID
cd $$BASEDIR
sleep 1 && echo 'Pelican and SimpleHTTPServer processes now running in background.'
}
###
# MAIN
###
[[ $$# -ne 1 ]] && usage
if [[ $$1 == "stop" ]]; then
shut_down
elif [[ $$1 == "restart" ]]; then
shut_down
start_up
elif [[ $$1 == "start" ]]; then
start_up
else
usage
fi

View file

@ -1,25 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- #
AUTHOR = u"$author"
SITENAME = u"$sitename"
SITEURL = '/'
TIMEZONE = 'Europe/Paris'
DEFAULT_LANG='$lang'
# Blogroll
LINKS = (
('Pelican', 'http://docs.notmyidea.org/alexis/pelican/'),
('Python.org', 'http://python.org'),
('Jinja2', 'http://jinja.pocoo.org'),
('You can modify those links in your config file', '#')
)
# Social widget
SOCIAL = (
('You can add links in your config file', '#'),
)
DEFAULT_PAGINATION = $default_pagination

View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- #
AUTHOR = $author
SITENAME = $sitename
SITEURL = ''
TIMEZONE = 'Europe/Paris'
DEFAULT_LANG = $lang
# Blogroll
LINKS = (('Pelican', 'http://docs.notmyidea.org/alexis/pelican/'),
('Python.org', 'http://python.org'),
('Jinja2', 'http://jinja.pocoo.org'),
('You can modify those links in your config file', '#'),)
# Social widget
SOCIAL = (('You can add links in your config file', '#'),
('Another social link', '#'),)
DEFAULT_PAGINATION = $default_pagination

View file

@ -0,0 +1,18 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- #
import sys
sys.path.append('.')
from pelicanconf import *
SITEURL = '$siteurl'
DELETE_OUTPUT_DIRECTORY = True
# Following items are often useful when publishing
# Uncomment following line for absolute URLs in production:
#RELATIVE_URLS = False
#DISQUS_SITENAME = ""
#GOOGLE_ANALYTICS = ""

View file

@ -6,7 +6,7 @@ import shutil
import logging
from collections import defaultdict
from codecs import open as _open
from codecs import open
from datetime import datetime
from itertools import groupby
from jinja2 import Markup
@ -15,6 +15,10 @@ from operator import attrgetter
logger = logging.getLogger(__name__)
class NoFilesError(Exception):
pass
def get_date(string):
"""Return a datetime object from a string.
@ -34,9 +38,9 @@ def get_date(string):
raise ValueError("'%s' is not a valid date" % string)
def open(filename):
def pelican_open(filename):
"""Open a file and return it's content"""
return _open(filename, encoding='utf-8').read()
return open(filename, encoding='utf-8').read()
def slugify(value):
@ -49,6 +53,8 @@ def slugify(value):
value = Markup(value).striptags()
if type(value) == unicode:
import unicodedata
from unidecode import unidecode
value = unicode(unidecode(value))
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
return re.sub('[-\s]+', '-', value)
@ -86,16 +92,40 @@ def copy(path, source, destination, destination_path=None, overwrite=False):
elif os.path.isfile(source_):
shutil.copy(source_, destination_)
logger.info('copying %s to %s' % (source_, destination_))
else:
logger.warning('skipped copy %s to %s' % (source_, destination_))
def clean_output_dir(path):
"""Remove all the files from the output directory"""
if not os.path.exists(path):
logger.debug("Directory already removed: %s" % path)
return
if not os.path.isdir(path):
try:
os.remove(path)
except Exception, e:
logger.error("Unable to delete file %s; %e" % path, e)
return
# remove all the existing content from the output folder
try:
shutil.rmtree(path)
except Exception:
pass
for filename in os.listdir(path):
file = os.path.join(path, filename)
if os.path.isdir(file):
try:
shutil.rmtree(file)
logger.debug("Deleted directory %s" % file)
except Exception, e:
logger.error("Unable to delete directory %s; %e" % file, e)
elif os.path.isfile(file) or os.path.islink(file):
try:
os.remove(file)
logger.debug("Deleted file/link %s" % file)
except Exception, e:
logger.error("Unable to delete file %s; %e" % file, e)
else:
logger.error("Unable to delete %s, file type unknown" % file)
def get_relative_path(filename):
@ -227,10 +257,13 @@ def files_changed(path, extensions):
yield os.stat(os.path.join(root, f)).st_mtime
global LAST_MTIME
mtime = max(file_times(path))
if mtime > LAST_MTIME:
LAST_MTIME = mtime
return True
try:
mtime = max(file_times(path))
if mtime > LAST_MTIME:
LAST_MTIME = mtime
return True
except ValueError:
raise NoFilesError("No files with the given extension(s) found.")
return False

View file

@ -148,9 +148,9 @@ class Writer(object):
paginators[key] = Paginator(object_list, len(object_list))
# generated pages, and write
name_root, ext = os.path.splitext(name)
for page_num in range(paginators.values()[0].num_pages):
paginated_localcontext = localcontext.copy()
paginated_name = name
for key in paginators.iterkeys():
paginator = paginators[key]
page = paginator.page(page_num + 1)
@ -158,9 +158,10 @@ class Writer(object):
{'%s_paginator' % key: paginator,
'%s_page' % key: page})
if page_num > 0:
ext = '.' + paginated_name.rsplit('.')[-1]
paginated_name = paginated_name.replace(ext,
'%s%s' % (page_num + 1, ext))
paginated_name = '%s%s%s' % (
name_root, page_num + 1, ext)
else:
paginated_name = name
_write_file(template, paginated_localcontext, self.output_path,
paginated_name)