1
0
Fork 0
forked from github/pelican

git merge upstream/master

This commit is contained in:
dave mankoff 2013-01-28 21:41:42 -05:00
commit bc2bc7a330
230 changed files with 7831 additions and 7255 deletions

View file

@ -1,3 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
import six
import os
import re
import sys
@ -8,70 +12,59 @@ import argparse
from pelican import signals
from pelican.generators import (ArticlesGenerator, PagesGenerator,
StaticGenerator, PdfGenerator, LessCSSGenerator)
StaticGenerator, PdfGenerator,
SourceFileGenerator, TemplatePagesGenerator)
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
__minor__ = 0
__version__ = "{0}.{1}".format(__major__, __minor__)
__minor__ = 2
__micro__ = 0
__version__ = "{0}.{1}.{2}".format(__major__, __minor__, __micro__)
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.ignore_files = settings['IGNORE_FILES']
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):
if isinstance(plugin, six.string_types):
logger.debug("Loading plugin `{0}' ...".format(plugin))
plugin = __import__(plugin, globals(), locals(), 'module')
logger.debug("Registering plugin `{0}' ...".format(plugin.__name__))
logger.debug("Registering plugin `{0}'".format(plugin.__name__))
plugin.register()
def _handle_deprecation(self):
@ -114,10 +107,41 @@ 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"""
context = self.settings.copy()
context['filenames'] = {} # share the dict between all the generators
context['localsiteurl'] = self.settings.get('SITEURL') # share
generators = [
cls(
context,
@ -126,7 +150,6 @@ class Pelican(object):
self.theme,
self.output_path,
self.markup,
self.delete_outputdir
) for cls in self.get_generator_classes()
]
@ -142,21 +165,33 @@ class Pelican(object):
writer = self.get_writer()
# pass the assets environment to the generators
if self.settings['WEBASSETS']:
generators[1].env.assets_environment = generators[0].assets_env
generators[2].env.assets_environment = generators[0].assets_env
for p in generators:
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['TEMPLATE_PAGES']:
generators.append(TemplatePagesGenerator)
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,31 +248,44 @@ 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):
if isinstance(cls, six.string_types):
module, cls_name = cls.rsplit('.', 1)
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
@ -245,8 +293,10 @@ def main():
# restriction; all files are recursively checked if they
# have changed, no matter what extension the filenames
# have.
if files_changed(pelican.path, pelican.markup) or \
files_changed(pelican.theme, ['']):
if files_changed(pelican.path, pelican.markup, pelican.ignore_files) or \
files_changed(pelican.theme, [''], pelican.ignore_files):
if not files_found_error:
files_found_error = True
pelican.run()
# reload also if settings.py changed
@ -258,11 +308,23 @@ 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 as e:
logger.warning(
"Caught exception \"{}\". Reloading.".format(e)
)
continue
else:
pelican.run()
except Exception, e:
logger.critical(unicode(e))
except Exception as e:
logger.critical(e)
if (args.verbosity == logging.DEBUG):
raise

View file

@ -1,19 +1,28 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
import six
import copy
import locale
import logging
import functools
import os
import re
import sys
from datetime import datetime
from os import getenv
from sys import platform, stdin
from pelican.settings import _DEFAULT_CONFIG
from pelican.utils import slugify, truncate_html_words
from pelican.utils import (slugify, truncate_html_words, memoized,
python_2_unicode_compatible, deprecated_attribute)
from pelican import signals
import pelican.utils
logger = logging.getLogger(__name__)
class Page(object):
"""Represents a page
Given a content, and metadata, create an adequate object.
@ -21,17 +30,23 @@ class Page(object):
:param content: the string to parse, containing the original content.
"""
mandatory_properties = ('title',)
default_template = 'page'
@deprecated_attribute(old='filename', new='source_path', since=(3, 2, 0))
def filename():
return None
def __init__(self, content, metadata=None, settings=None,
filename=None):
source_path=None, context=None):
# init parameters
if not metadata:
metadata = {}
if not settings:
settings = _DEFAULT_CONFIG
settings = copy.deepcopy(_DEFAULT_CONFIG)
self.settings = settings
self._content = content
self._context = context
self.translations = []
local_metadata = dict(settings.get('DEFAULT_METADATA', ()))
@ -44,15 +59,13 @@ 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:
self.author = Author(settings['AUTHOR'], settings)
else:
title = filename.decode('utf-8') if filename else self.title
self.author = Author(getenv('USER', 'John Doe'), settings)
logger.warning(u"Author of `{0}' unknown, assuming that his name is "
"`{1}'".format(title, self.author))
# manage languages
self.in_default_lang = True
@ -67,8 +80,8 @@ class Page(object):
if not hasattr(self, 'slug') and hasattr(self, 'title'):
self.slug = slugify(self.title)
if filename:
self.filename = filename
if source_path:
self.source_path = source_path
# manage the date format
if not hasattr(self, 'date_format'):
@ -78,17 +91,15 @@ class Page(object):
self.date_format = settings['DEFAULT_DATE_FORMAT']
if isinstance(self.date_format, tuple):
locale.setlocale(locale.LC_ALL, self.date_format[0])
locale_string = self.date_format[0]
if sys.version_info < (3, ) and isinstance(locale_string, six.text_type):
locale_string = locale_string.encode('ascii')
locale.setlocale(locale.LC_ALL, locale_string)
self.date_format = self.date_format[1]
if hasattr(self, 'date'):
encoded_date = self.date.strftime(
self.date_format.encode('ascii', 'xmlcharrefreplace'))
if platform == 'win32':
self.locale_date = encoded_date.decode(stdin.encoding)
else:
self.locale_date = encoded_date.decode('utf')
self.locale_date = pelican.utils.strftime(self.date,
self.date_format)
# manage status
if not hasattr(self, 'status'):
@ -101,6 +112,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:
@ -109,13 +122,16 @@ class Page(object):
@property
def url_format(self):
return {
metadata = copy.copy(self.metadata)
metadata.update({
'slug': getattr(self, 'slug', ''),
'lang': getattr(self, 'lang', 'en'),
'date': getattr(self, 'date', datetime.now()),
'author': self.author,
'category': getattr(self, 'category', 'misc'),
}
'author': getattr(self, 'author', ''),
'category': getattr(self, 'category',
self.settings['DEFAULT_CATEGORY']),
})
return metadata
def _expand_settings(self, key):
fq_key = ('%s_%s' % (self.__class__.__name__, key)).upper()
@ -125,13 +141,60 @@ class Page(object):
key = key if self.in_default_lang else 'lang_%s' % key
return self._expand_settings(key)
def _update_content(self, content, siteurl):
"""Change all the relative paths of the content to relative paths
suitable for the ouput content.
:param content: content resource that will be passed to the templates.
:param siteurl: siteurl which is locally generated by the writer in
case of RELATIVE_URLS.
"""
hrefs = re.compile(r"""
(?P<markup><\s*[^\>]* # match tag with src and href attr
(?:href|src)\s*=)
(?P<quote>["\']) # require value to be quoted
(?P<path>\|(?P<what>.*?)\|(?P<value>.*?)) # the url value
\2""", re.X)
def replacer(m):
what = m.group('what')
value = m.group('value')
origin = m.group('path')
# we support only filename for now. the plan is to support
# categories, tags, etc. in the future, but let's keep things
# simple for now.
if what == 'filename':
if value.startswith('/'):
value = value[1:]
else:
# relative to the source path of this content
value = self.get_relative_source_path(
os.path.join(self.relative_dir, value)
)
if value in self._context['filenames']:
origin = '/'.join((siteurl,
self._context['filenames'][value].url))
else:
logger.warning("Unable to find {fn}, skipping url"
" replacement".format(fn=value))
return m.group('markup') + m.group('quote') + origin \
+ m.group('quote')
return hrefs.sub(replacer, content)
@memoized
def get_content(self, siteurl):
return self._update_content(
self._get_content() if hasattr(self, "_get_content")
else self._content,
siteurl)
@property
def content(self):
if hasattr(self, "_get_content"):
content = self._get_content()
else:
content = self._content
return content
return self.get_content(self._context['localsiteurl'])
def _get_summary(self):
"""Returns the summary of an article, based on the summary metadata
@ -140,7 +203,8 @@ class Page(object):
return self._summary
else:
if self.settings['SUMMARY_MAX_LENGTH']:
return truncate_html_words(self.content, self.settings['SUMMARY_MAX_LENGTH'])
return truncate_html_words(self.content,
self.settings['SUMMARY_MAX_LENGTH'])
return self.content
def _set_summary(self, summary):
@ -153,18 +217,49 @@ 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
def get_relative_source_path(self, source_path=None):
"""Return the relative path (from the content path) to the given
source_path.
If no source path is specified, use the source path of this
content object.
"""
if not source_path:
source_path = self.source_path
return os.path.relpath(
os.path.abspath(os.path.join(self.settings['PATH'], source_path)),
os.path.abspath(self.settings['PATH'])
)
@property
def relative_dir(self):
return os.path.dirname(os.path.relpath(
os.path.abspath(self.source_path),
os.path.abspath(self.settings['PATH']))
)
class Article(Page):
mandatory_properties = ('title', 'date', 'category')
default_template = 'article'
class Quote(Page):
base_properties = ('author', 'date')
@python_2_unicode_compatible
@functools.total_ordering
class URLWrapper(object):
def __init__(self, name, settings):
self.name = unicode(name)
self.name = name
self.slug = slugify(self.name)
self.settings = settings
@ -174,24 +269,41 @@ class URLWrapper(object):
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
return self.name == unicode(other)
def __str__(self):
return str(self.name.encode('utf-8', 'replace'))
def __unicode__(self):
def _key(self):
return self.name
def _from_settings(self, key):
def _normalize_key(self, key):
return six.text_type(key)
def __eq__(self, other):
return self._key() == self._normalize_key(other)
def __ne__(self, other):
return self._key() != self._normalize_key(other)
def __lt__(self, other):
return self._key() < self._normalize_key(other)
def __str__(self):
return self.name
def _from_settings(self, key, get_page_name=False):
"""Returns URL information as defined in settings.
When get_page_name=True returns URL without anything after {slug}
e.g. if in settings: CATEGORY_URL="cat/{slug}.html" this returns "cat/{slug}"
Useful for pagination."""
setting = "%s_%s" % (self.__class__.__name__.upper(), key)
value = self.settings[setting]
if not isinstance(value, basestring):
logger.warning(u'%s is set to %s' % (setting, value))
if not isinstance(value, six.string_types):
logger.warning('%s is set to %s' % (setting, value))
return value
else:
return unicode(value).format(**self.as_dict())
if get_page_name:
return os.path.splitext(value)[0].format(**self.as_dict())
else:
return value.format(**self.as_dict())
page_name = property(functools.partial(_from_settings, key='URL', get_page_name=True))
url = property(functools.partial(_from_settings, key='URL'))
save_as = property(functools.partial(_from_settings, key='SAVE_AS'))
@ -202,18 +314,36 @@ class Category(URLWrapper):
class Tag(URLWrapper):
def __init__(self, name, *args, **kwargs):
super(Tag, self).__init__(unicode.strip(name), *args, **kwargs)
super(Tag, self).__init__(name.strip(), *args, **kwargs)
class Author(URLWrapper):
pass
@python_2_unicode_compatible
class StaticContent(object):
@deprecated_attribute(old='filepath', new='source_path', since=(3, 2, 0))
def filepath():
return None
def __init__(self, src, dst=None, settings=None):
if not settings:
settings = copy.deepcopy(_DEFAULT_CONFIG)
self.src = src
self.url = dst or src
self.source_path = os.path.join(settings['PATH'], src)
self.save_as = os.path.join(settings['OUTPUT_PATH'], self.url)
def __str__(self):
return self.source_path
def is_valid_content(content, f):
try:
content.check_properties()
return True
except NameError, e:
logger.error(u"Skipping %s: impossible to find informations about '%s'"\
% (f, e))
except NameError as e:
logger.error("Skipping %s: impossible to find informations about "
"'%s'" % (f, e))
return False

View file

@ -1,22 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
import os
import math
import random
import logging
import datetime
import subprocess
import shutil
from codecs import open
from collections import defaultdict
from functools import partial
from itertools import chain
from operator import attrgetter, itemgetter
from jinja2 import Environment, FileSystemLoader, PrefixLoader, ChoiceLoader
from jinja2.exceptions import TemplateNotFound
from jinja2 import (Environment, FileSystemLoader, PrefixLoader, ChoiceLoader,
BaseLoader, TemplateNotFound)
from pelican.contents import Article, Page, Category, is_valid_content
from pelican.contents import Article, Page, Category, StaticContent, \
is_valid_content
from pelican.readers import read_file
from pelican.utils import copy, process_translations, open
from pelican.utils import copy, process_translations, mkdir_p
from pelican import signals
@ -36,14 +40,17 @@ 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__))
simple_loader = FileSystemLoader(os.path.join(theme_path,
"themes", "simple", "templates"))
self.env = Environment(
trim_blocks=True,
loader=ChoiceLoader([
FileSystemLoader(self._templates_path),
simple_loader, # implicit inheritance
@ -58,6 +65,8 @@ class Generator(object):
custom_filters = self.settings.get('JINJA_FILTERS', {})
self.env.filters.update(custom_filters)
signals.generator_init.send(self)
def get_template(self, name):
"""Return the template by name.
Use self.theme to get the templates to use, and return a list of
@ -76,8 +85,10 @@ class Generator(object):
:param path: the path to search the file on
:param exclude: the list of path to exclude
:param extensions: the list of allowed extensions (if False, all
extensions are allowed)
"""
if not extensions:
if extensions is None:
extensions = self.markup
files = []
@ -91,10 +102,17 @@ class Generator(object):
for e in exclude:
if e in dirs:
dirs.remove(e)
files.extend([os.sep.join((root, f)) for f in temp_files
if True in [f.endswith(ext) for ext in extensions]])
for f in temp_files:
if extensions is False or \
(True in [f.endswith(ext) for ext in extensions]):
files.append(os.sep.join((root, f)))
return files
def add_source_path(self, content):
location = os.path.relpath(os.path.abspath(content.source_path),
os.path.abspath(self.path))
self.context['filenames'][location] = content
def _update_context(self, items):
"""Update the context with the given items from the currrent
processor.
@ -102,10 +120,39 @@ class Generator(object):
for item in items:
value = getattr(self, item)
if hasattr(value, 'items'):
value = value.items()
value = list(value.items()) # py3k safeguard for iterators
self.context[item] = value
class _FileLoader(BaseLoader):
def __init__(self, path, basedir):
self.path = path
self.fullpath = os.path.join(basedir, path)
def get_source(self, environment, template):
if template != self.path or not os.path.exists(self.fullpath):
raise TemplateNotFound(template)
mtime = os.path.getmtime(self.fullpath)
with open(self.fullpath, 'r', encoding='utf-8') as f:
source = f.read()
return source, self.fullpath, \
lambda: mtime == os.path.getmtime(self.fullpath)
class TemplatePagesGenerator(Generator):
def generate_output(self, writer):
for source, dest in self.settings['TEMPLATE_PAGES'].items():
self.env.loader.loaders.insert(0, _FileLoader(source, self.path))
try:
template = self.env.get_template(source)
rurls = self.settings.get('RELATIVE_URLS')
writer.write_file(dest, template, self.context, rurls)
finally:
del self.env.loader.loaders[0]
class ArticlesGenerator(Generator):
"""Generate blog articles"""
@ -116,6 +163,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 = []
@ -124,54 +172,74 @@ class ArticlesGenerator(Generator):
def generate_feeds(self, writer):
"""Generate the feeds from the current context, and output files."""
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,
self.settings['FEED_RSS'], feed_type='rss')
if self.settings.get('FEED_ALL_ATOM') or \
self.settings.get('FEED_ALL_RSS'):
all_articles = list(self.articles)
for article in self.articles:
all_articles.extend(article.translations)
all_articles.sort(key=attrgetter('date'), reverse=True)
if self.settings.get('FEED_ALL_ATOM'):
writer.write_feed(all_articles, self.context,
self.settings['FEED_ALL_ATOM'])
if self.settings.get('FEED_ALL_RSS'):
writer.write_feed(all_articles, self.context,
self.settings['FEED_ALL_RSS'], feed_type='rss')
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 +251,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,
@ -198,7 +266,7 @@ class ArticlesGenerator(Generator):
write(tag.save_as, tag_template, self.context, tag=tag,
articles=articles, dates=dates,
paginated={'articles': articles, 'dates': dates},
page_name=u'tag/%s' % tag)
page_name=tag.page_name)
def generate_categories(self, write):
"""Generate category pages."""
@ -208,7 +276,7 @@ class ArticlesGenerator(Generator):
write(cat.save_as, category_template, self.context,
category=cat, articles=articles, dates=dates,
paginated={'articles': articles, 'dates': dates},
page_name=u'category/%s' % cat)
page_name=cat.page_name)
def generate_authors(self, write):
"""Generate Author pages."""
@ -218,14 +286,14 @@ class ArticlesGenerator(Generator):
write(aut.save_as, author_template, self.context,
author=aut, articles=articles, dates=dates,
paginated={'articles': articles, 'dates': dates},
page_name=u'author/%s' % aut)
page_name=aut.page_name)
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"""
@ -237,7 +305,6 @@ class ArticlesGenerator(Generator):
self.generate_articles(write)
self.generate_direct_templates(write)
# and subfolders after that
self.generate_tags(write)
self.generate_categories(write)
@ -245,7 +312,7 @@ class ArticlesGenerator(Generator):
self.generate_drafts(write)
def generate_context(self):
"""change the context"""
"""Add the articles into the shared context"""
article_path = os.path.normpath( # we have to remove trailing slashes
os.path.join(self.path, self.settings['ARTICLE_DIR'])
@ -255,33 +322,42 @@ class ArticlesGenerator(Generator):
article_path,
exclude=self.settings['ARTICLE_EXCLUDES']):
try:
signals.article_generate_preread.send(self)
content, metadata = read_file(f, settings=self.settings)
except Exception, e:
logger.warning(u'Could not process %s\n%s' % (f, str(e)))
except Exception as e:
logger.warning('Could not process %s\n%s' % (f, str(e)))
continue
# if no category is set, use the name of the path as a category
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']
if (self.settings['USE_FOLDER_AS_CATEGORY']
and os.path.dirname(f) != article_path):
# if the article is in a subdirectory
category = os.path.basename(os.path.dirname(f))
else:
category = os.path.basename(os.path.dirname(f))\
.decode('utf-8')
# if the article is not in a subdirectory
category = self.settings['DEFAULT_CATEGORY']
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.get('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,
filename=f)
source_path=f, context=self.context)
if not is_valid_content(article, f):
continue
self.add_source_path(article)
if article.status == "published":
if hasattr(article, 'tags'):
for tag in article.tags:
@ -290,8 +366,8 @@ class ArticlesGenerator(Generator):
elif article.status == "draft":
self.drafts.append(article)
else:
logger.warning(u"Unknown status %s for file %s, skipping it." %
(repr(unicode.encode(article.status, 'utf-8')),
logger.warning("Unknown status %s for file %s, skipping it." %
(repr(article.status),
repr(f)))
self.articles, self.translations = process_translations(all_articles)
@ -299,13 +375,15 @@ class ArticlesGenerator(Generator):
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)
# ignore blank authors as well as undefined
if hasattr(article,'author') and article.author.name != '':
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'])
reverse=self.context['NEWEST_FIRST_ARCHIVES'])
# create tag cloud
tag_cloud = defaultdict(int)
@ -316,7 +394,7 @@ class ArticlesGenerator(Generator):
tag_cloud = sorted(tag_cloud.items(), key=itemgetter(1), reverse=True)
tag_cloud = tag_cloud[:self.settings.get('TAG_CLOUD_MAX_ITEMS')]
tags = map(itemgetter(1), tag_cloud)
tags = list(map(itemgetter(1), tag_cloud))
if tags:
max_count = max(tags)
steps = self.settings.get('TAG_CLOUD_STEPS')
@ -338,14 +416,15 @@ class ArticlesGenerator(Generator):
# order the categories per name
self.categories = list(self.categories.items())
self.categories.sort(
key=lambda item: item[0].name,
reverse=self.settings['REVERSE_CATEGORY_ORDER'])
self.authors = list(self.authors.items())
self.authors.sort(key=lambda item: item[0].name)
self.authors.sort()
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)
@ -360,6 +439,7 @@ class PagesGenerator(Generator):
self.hidden_pages = []
self.hidden_translations = []
super(PagesGenerator, self).__init__(*args, **kwargs)
signals.pages_generator_init.send(self)
def generate_context(self):
all_pages = []
@ -368,24 +448,27 @@ class PagesGenerator(Generator):
os.path.join(self.path, self.settings['PAGE_DIR']),
exclude=self.settings['PAGE_EXCLUDES']):
try:
content, metadata = read_file(f)
except Exception, e:
logger.error(u'Could not process %s\n%s' % (f, str(e)))
content, metadata = read_file(f, settings=self.settings)
except Exception as e:
logger.warning('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)
source_path=f, context=self.context)
if not is_valid_content(page, f):
continue
self.add_source_path(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')),
logger.warning("Unknown status %s for file %s, skipping it." %
(repr(page.status),
repr(f)))
self.pages, self.translations = process_translations(all_pages)
self.hidden_pages, self.hidden_translations = process_translations(hidden_pages)
@ -395,7 +478,7 @@ class PagesGenerator(Generator):
def generate_output(self, writer):
for page in chain(self.translations, self.pages,
self.hidden_translations, self.hidden_pages):
writer.write_file(page.save_as, self.get_template('page'),
writer.write_file(page.save_as, self.get_template(page.template),
self.context, page=page,
relative_urls=self.settings.get('RELATIVE_URLS'))
@ -412,53 +495,61 @@ class StaticGenerator(Generator):
final_path, overwrite=True)
def generate_context(self):
self.staticfiles = []
if self.settings['WEBASSETS']:
from webassets import Environment as AssetsEnvironment
# 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/'
assets_src = os.path.join(self.output_path, 'theme')
self.assets_env = AssetsEnvironment(assets_src, assets_url)
if logging.getLevelName(logger.getEffectiveLevel()) == "DEBUG":
self.assets_env.debug = True
# walk static paths
for static_path in self.settings['STATIC_PATHS']:
for f in self.get_files(
os.path.join(self.path, static_path), extensions=False):
f_rel = os.path.relpath(f, self.path)
# TODO remove this hardcoded 'static' subdirectory
sc = StaticContent(f_rel, os.path.join('static', f_rel),
settings=self.settings)
self.staticfiles.append(sc)
self.context['filenames'][f_rel] = sc
# same thing for FILES_TO_COPY
for src, dest in self.settings['FILES_TO_COPY']:
sc = StaticContent(src, dest, settings=self.settings)
self.staticfiles.append(sc)
self.context['filenames'][src] = sc
def generate_output(self, writer):
self._copy_paths(self.settings['STATIC_PATHS'], self.path,
'static', self.output_path)
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)
# copy all StaticContent files
for sc in self.staticfiles:
mkdir_p(os.path.dirname(sc.save_as))
shutil.copy(sc.source_path, sc.save_as)
logger.info('copying {} to {}'.format(sc.source_path, sc.save_as))
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"):
if obj.source_path.endswith('.rst'):
filename = obj.slug + ".pdf"
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)
logger.info(u' [ok] writing %s' % output_pdf)
# print('Generating pdf for', obj.source_path, 'in', output_pdf)
with open(obj.source_path) as f:
self.pdfcreator.createPdf(text=f.read(), output=output_pdf)
logger.info(' [ok] writing %s' % output_pdf)
def generate_context(self):
pass
@ -466,14 +557,14 @@ class PdfGenerator(Generator):
def generate_output(self, writer=None):
# we don't use the writer passed as argument here
# since we write our own files
logger.info(u' Generating PDF files...')
logger.info(' Generating PDF files...')
pdf_path = os.path.join(self.output_path, 'pdf')
if not os.path.exists(pdf_path):
try:
os.mkdir(pdf_path)
except OSError:
logger.error("Couldn't create the pdf output folder in " + pdf_path)
pass
logger.error("Couldn't create the pdf output folder in " +
pdf_path)
for article in self.context['articles']:
self._create_pdf(article, pdf_path)
@ -481,49 +572,16 @@ 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']
class LessCSSGenerator(Generator):
"""Compile less css files."""
def _compile(self, less_file, source_dir, dest_dir):
base = os.path.relpath(less_file, source_dir)
target = os.path.splitext(
os.path.join(dest_dir, base))[0] + '.css'
target_dir = os.path.dirname(target)
if not os.path.exists(target_dir):
try:
os.makedirs(target_dir)
except OSError:
logger.error("Couldn't create the less css output folder in " +
target_dir)
subprocess.call([self._lessc, less_file, target])
logger.info(u' [ok] compiled %s' % base)
def _create_source(self, obj, output_path):
output_path = os.path.splitext(obj.save_as)[0]
dest = os.path.join(output_path, output_path + self.output_extension)
copy('', obj.source_path, dest)
def generate_output(self, writer=None):
logger.info(u' Compiling less css')
# store out compiler here, so it won't be evaulted on each run of
# _compile
lg = self.settings['LESS_GENERATOR']
self._lessc = lg if isinstance(lg, basestring) else 'lessc'
# walk static paths
for static_path in self.settings['STATIC_PATHS']:
for f in self.get_files(
os.path.join(self.path, static_path),
extensions=['less']):
self._compile(f, self.path, self.output_path)
# walk theme static paths
theme_output_path = os.path.join(self.output_path, 'theme')
for static_path in self.settings['THEME_STATIC_PATHS']:
theme_static_path = os.path.join(self.theme, static_path)
for f in self.get_files(
theme_static_path,
extensions=['less']):
self._compile(f, theme_static_path, theme_output_path)
logger.info(' Generating source files...')
for object in chain(self.context['articles'], self.context['pages']):
self._create_source(object, self.output_path)

View file

@ -1,3 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
__all__ = [
'init'
]
@ -9,7 +12,7 @@ import logging
from logging import Formatter, getLogger, StreamHandler, DEBUG
RESET_TERM = u'\033[0;m'
RESET_TERM = '\033[0;m'
COLOR_CODES = {
'red': 31,
@ -24,37 +27,38 @@ COLOR_CODES = {
def ansi(color, text):
"""Wrap text in an ansi escape sequence"""
code = COLOR_CODES[color]
return u'\033[1;{0}m{1}{2}'.format(code, text, RESET_TERM)
return '\033[1;{0}m{1}{2}'.format(code, text, RESET_TERM)
class ANSIFormatter(Formatter):
"""
Convert a `logging.LogReport' object into colored text, using ANSI escape sequences.
Convert a `logging.LogRecord' object into colored text, using ANSI escape sequences.
"""
## colors:
def format(self, record):
if record.levelname is 'INFO':
return ansi('cyan', '-> ') + unicode(record.msg)
elif record.levelname is 'WARNING':
return ansi('yellow', record.levelname) + ': ' + unicode(record.msg)
elif record.levelname is 'ERROR':
return ansi('red', record.levelname) + ': ' + unicode(record.msg)
elif record.levelname is 'CRITICAL':
return ansi('bgred', record.levelname) + ': ' + unicode(record.msg)
elif record.levelname is 'DEBUG':
return ansi('bggrey', record.levelname) + ': ' + unicode(record.msg)
msg = str(record.msg)
if record.levelname == 'INFO':
return ansi('cyan', '-> ') + msg
elif record.levelname == 'WARNING':
return ansi('yellow', record.levelname) + ': ' + msg
elif record.levelname == 'ERROR':
return ansi('red', record.levelname) + ': ' + msg
elif record.levelname == 'CRITICAL':
return ansi('bgred', record.levelname) + ': ' + msg
elif record.levelname == 'DEBUG':
return ansi('bggrey', record.levelname) + ': ' + msg
else:
return ansi('white', record.levelname) + ': ' + unicode(record.msg)
return ansi('white', record.levelname) + ': ' + msg
class TextFormatter(Formatter):
"""
Convert a `logging.LogReport' object into text.
Convert a `logging.LogRecord' object into text.
"""
def format(self, record):
if not record.levelname or record.levelname is 'INFO':
if not record.levelname or record.levelname == 'INFO':
return record.msg
else:
return record.levelname + ': ' + record.msg

View file

@ -1,3 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
# From django.core.paginator
from math import ceil
@ -37,7 +40,7 @@ class Paginator(object):
Returns a 1-based range of pages for iterating through within
a template for loop.
"""
return range(1, self.num_pages + 1)
return list(range(1, self.num_pages + 1))
page_range = property(_get_page_range)

53
pelican/plugins/assets.py Normal file
View file

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
"""
Asset management plugin for Pelican
===================================
This plugin allows you to use the `webassets`_ module to manage assets such as
CSS and JS files.
The ASSET_URL is set to a relative url to honor Pelican's RELATIVE_URLS
setting. This requires the use of SITEURL in the templates::
<link rel="stylesheet" href="{{ SITEURL }}/{{ ASSET_URL }}">
.. _webassets: https://webassets.readthedocs.org/
"""
import os
import logging
from pelican import signals
from webassets import Environment
from webassets.ext.jinja2 import AssetsExtension
def add_jinja2_ext(pelican):
"""Add Webassets to Jinja2 extensions in Pelican settings."""
pelican.settings['JINJA_EXTENSIONS'].append(AssetsExtension)
def create_assets_env(generator):
"""Define the assets environment and pass it to the generator."""
assets_url = 'theme/'
assets_src = os.path.join(generator.output_path, 'theme')
generator.env.assets_environment = Environment(assets_src, assets_url)
if 'ASSET_CONFIG' in generator.settings:
for item in generator.settings['ASSET_CONFIG']:
generator.env.assets_environment.config[item[0]] = item[1]
logger = logging.getLogger(__name__)
if logging.getLevelName(logger.getEffectiveLevel()) == "DEBUG":
generator.env.assets_environment.debug = True
def register():
"""Plugin registration."""
signals.initialized.connect(add_jinja2_ext)
signals.generator_init.connect(create_assets_env)

View file

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
"""
Copyright (c) Marco Milanesi <kpanic@gnufunk.org>

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,78 @@
# Copyright (c) 2012 Matt Layman
'''A plugin to create .gz cache files for optimization.'''
import gzip
import logging
import os
from pelican import signals
logger = logging.getLogger(__name__)
# A list of file types to exclude from possible compression
EXCLUDE_TYPES = [
# Compressed types
'.bz2',
'.gz',
# Audio types
'.aac',
'.flac',
'.mp3',
'.wma',
# Image types
'.gif',
'.jpg',
'.jpeg',
'.png',
# Video types
'.avi',
'.mov',
'.mp4',
]
def create_gzip_cache(pelican):
'''Create a gzip cache file for every file that a webserver would
reasonably want to cache (e.g., text type files).
:param pelican: The Pelican instance
'''
for dirpath, _, filenames in os.walk(pelican.settings['OUTPUT_PATH']):
for name in filenames:
if should_compress(name):
filepath = os.path.join(dirpath, name)
create_gzip_file(filepath)
def should_compress(filename):
'''Check if the filename is a type of file that should be compressed.
:param filename: A file name to check against
'''
for extension in EXCLUDE_TYPES:
if filename.endswith(extension):
return False
return True
def create_gzip_file(filepath):
'''Create a gzipped file in the same directory with a filepath.gz name.
:param filepath: A file to compress
'''
compressed_path = filepath + '.gz'
with open(filepath, 'rb') as uncompressed:
try:
logger.debug('Compressing: %s' % filepath)
compressed = gzip.open(compressed_path, 'wb')
compressed.writelines(uncompressed)
except Exception as ex:
logger.critical('Gzip compression failed: %s' % ex)
finally:
compressed.close()
def register():
signals.finalized.connect(create_gzip_cache)

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from docutils import nodes
from docutils.parsers.rst import directives, Directive
from pelican import log
"""
HTML tags for reStructuredText
@ -52,7 +53,7 @@ class RawHtml(Directive):
has_content = True
def run(self):
html = u' '.join(self.content)
html = ' '.join(self.content)
node = nodes.raw('', html, format='html')
return [node]

View file

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
from pelican import signals
def test(sender):
print "%s initialized !!" % sender
print("%s initialized !!" % sender)
def register():
signals.initialized.connect(test)

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(list(zip(set(related_posts), list(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)

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

@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
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 = """{0}/index.html
{0}/archives.html
{0}/tags.html
{0}/categories.html
"""
XML_HEADER = """<?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 = """
<url>
<loc>{0}/{1}</loc>
<lastmod>{2}</lastmod>
<changefreq>{3}</changefreq>
<priority>{4}</priority>
</url>
"""
XML_FOOTER = """
</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):
# We use items for Py3k compat. .iteritems() otherwise
for k, v in pris.items():
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):
# .items() for py3k compat.
for k, v in chfreqs.items():
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

@ -1,4 +1,9 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
import six
import os
import re
try:
import docutils
import docutils.core
@ -13,19 +18,24 @@ try:
from markdown import Markdown
except ImportError:
Markdown = False # NOQA
try:
from asciidocapi import AsciiDocAPI
asciidoc = True
except ImportError:
asciidoc = False
import re
import cgi
from HTMLParser import HTMLParser
from pelican.contents import Category, Tag, Author
from pelican.utils import get_date, open
from pelican.utils import get_date, pelican_open
_METADATA_PROCESSORS = {
'tags': lambda x, y: [Tag(tag, y) for tag in unicode(x).split(',')],
'tags': lambda x, y: [Tag(tag, y) for tag in x.split(',')],
'date': lambda x, y: get_date(x),
'status': lambda x, y: unicode.strip(x),
'status': lambda x, y: x.strip(),
'category': Category,
'author': Author,
}
@ -66,6 +76,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,19 +112,20 @@ class RstReader(Reader):
output[name] = self.process_metadata(name, value)
return output
def _get_publisher(self, filename):
def _get_publisher(self, source_path):
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.set_source(source_path=source_path)
pub.publish()
return pub
def read(self, filename):
def read(self, source_path):
"""Parses restructured text"""
pub = self._get_publisher(filename)
pub = self._get_publisher(source_path)
parts = pub.writer.parts
content = parts.get('body')
@ -117,16 +140,28 @@ class MarkdownReader(Reader):
file_extensions = ['md', 'markdown', 'mkd']
extensions = ['codehilite', 'extra']
def read(self, filename):
def _parse_metadata(self, meta):
"""Return the dict containing document metadata"""
md = Markdown(extensions=set(self.extensions + ['meta']))
output = {}
for name, value in meta.items():
name = name.lower()
if name == "summary":
summary_values = "\n".join(str(item) for item in value)
summary = md.convert(summary_values)
output[name] = self.process_metadata(name, summary)
else:
output[name] = self.process_metadata(name, value[0])
return output
def read(self, source_path):
"""Parse content and metadata of markdown files"""
with open(filename) as text:
with pelican_open(source_path) as text:
md = Markdown(extensions=set(self.extensions + ['meta']))
content = md.convert(text)
metadata = {}
for name, value in md.Meta.items():
name = name.lower()
metadata[name] = self.process_metadata(name, value[0])
metadata = self._parse_metadata(md.Meta)
return content, metadata
class HTMLReader(Reader):
@ -223,7 +258,7 @@ class HTMLReader(Reader):
def read(self, filename):
"""Parse content and metadata of markdown files"""
with open(filename) as content:
with pelican_open(filename) as content:
parser = self._HTMLParser(self.settings)
parser.feed(content)
parser.close()
@ -233,6 +268,37 @@ class HTMLReader(Reader):
metadata[k] = self.process_metadata(k, parser.metadata[k])
return parser.body, metadata
class AsciiDocReader(Reader):
enabled = bool(asciidoc)
file_extensions = ['asc']
default_options = ["--no-header-footer", "-a newline=\\n"]
def read(self, source_path):
"""Parse content and metadata of asciidoc files"""
from cStringIO import StringIO
text = StringIO(pelican_open(source_path))
content = StringIO()
ad = AsciiDocAPI()
options = self.settings.get('ASCIIDOC_OPTIONS', [])
if isinstance(options, (str, unicode)):
options = [m.strip() for m in options.split(',')]
options = self.default_options + options
for o in options:
ad.options(*o.split())
ad.execute(text, content, backend="html4")
content = content.getvalue()
metadata = {}
for name, value in ad.asciidoc.document.attributes.items():
name = name.lower()
metadata[name] = self.process_metadata(name, value)
if 'doctitle' in metadata:
metadata['title'] = metadata['doctitle']
return content, metadata
_EXTENSIONS = {}
for cls in Reader.__subclasses__():
@ -240,13 +306,14 @@ for cls in Reader.__subclasses__():
_EXTENSIONS[ext] = cls
def read_file(filename, fmt=None, settings=None):
def read_file(path, fmt=None, settings=None):
"""Return a reader object using the given format."""
base, ext = os.path.splitext(os.path.basename(path))
if not fmt:
fmt = filename.split('.')[-1]
fmt = ext[1:]
if fmt not in _EXTENSIONS:
raise TypeError('Pelican does not know how to parse %s' % filename)
raise TypeError('Pelican does not know how to parse {}'.format(path))
reader = _EXTENSIONS[fmt](settings)
settings_key = '%s_EXTENSIONS' % fmt.upper()
@ -257,12 +324,22 @@ def read_file(filename, fmt=None, settings=None):
if not reader.enabled:
raise ValueError("Missing dependencies for %s" % fmt)
content, metadata = reader.read(filename)
content, metadata = reader.read(path)
# 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'])
if settings and settings.get('TYPOGRIFY'):
from typogrify.filters import typogrify
content = typogrify(content)
metadata['title'] = typogrify(metadata['title'])
file_metadata = settings and settings.get('FILENAME_METADATA')
if file_metadata:
match = re.match(file_metadata, base)
if match:
# .items() for py3k compat.
for k, v in match.groupdict().items():
if k not in metadata:
k = k.lower() # metadata must be lowercase
metadata[k] = reader.process_metadata(k, v)
return content, metadata

View file

@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
from docutils import nodes
from docutils.parsers.rst import directives, Directive
from __future__ import unicode_literals, print_function
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)
@ -31,7 +34,7 @@ class Pygments(Directive):
# take an arbitrary option if more than one is given
formatter = self.options and VARIANTS[self.options.keys()[0]] \
or DEFAULT
parsed = highlight(u'\n'.join(self.content), lexer, formatter)
parsed = highlight('\n'.join(self.content), lexer, formatter)
return [nodes.raw('', parsed, format='html')]
directives.register_directive('code-block', Pygments)
@ -94,3 +97,21 @@ 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)

20
pelican/server.py Normal file
View file

@ -0,0 +1,20 @@
from __future__ import print_function
try:
import SimpleHTTPServer as srvmod
except ImportError:
import http.server as srvmod
try:
import SocketServer as socketserver
except ImportError:
import socketserver
PORT = 8000
Handler = srvmod.SimpleHTTPRequestHandler
httpd = socketserver.TCPServer(("", PORT), Handler)
print("serving at port", PORT)
httpd.serve_forever()

View file

@ -1,4 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
import six
import copy
import imp
import inspect
import os
import locale
import logging
@ -21,18 +27,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_ALL_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',
'USE_FOLDER_AS_CATEGORY': True,
'DEFAULT_CATEGORY': 'misc',
'FALLBACK_ON_FS_DATE': True,
'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',
@ -47,13 +56,14 @@ _DEFAULT_CONFIG = {'PATH': '.',
'CATEGORY_SAVE_AS': 'category/{slug}.html',
'TAG_URL': 'tag/{slug}.html',
'TAG_SAVE_AS': 'tag/{slug}.html',
'AUTHOR_URL': u'author/{slug}.html',
'AUTHOR_SAVE_AS': u'author/{slug}.html',
'AUTHOR_URL': 'author/{slug}.html',
'AUTHOR_SAVE_AS': 'author/{slug}.html',
'RELATIVE_URLS': True,
'DEFAULT_LANG': 'en',
'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',
@ -63,59 +73,83 @@ _DEFAULT_CONFIG = {'PATH': '.',
'DEFAULT_PAGINATION': False,
'DEFAULT_ORPHANS': 0,
'DEFAULT_METADATA': (),
'FILENAME_METADATA': '(?P<date>\d{4}-\d{2}-\d{2}).*',
'FILES_TO_COPY': (),
'DEFAULT_STATUS': 'published',
'ARTICLE_PERMALINK_STRUCTURE': '',
'TYPOGRIFY': False,
'LESS_GENERATOR': False,
'SUMMARY_MAX_LENGTH': 50,
'WEBASSETS': False,
'PLUGINS': [],
'TEMPLATE_PAGES': {},
'IGNORE_FILES': []
}
def read_settings(filename=None):
if filename:
local_settings = get_settings_from_file(filename)
def read_settings(path=None, override=None):
if path:
local_settings = get_settings_from_file(path)
# 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(path), local_settings[p])))
if p != 'THEME' or os.path.exists(absp):
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(path, 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(path).rpartition('.')[0]
module = imp.load_source(name, path)
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']
if isinstance(locales, basestring):
if isinstance(locales, six.string_types):
locales = [locales]
# try to set the different locales, fallback on the default.
@ -124,8 +158,8 @@ 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
locale.setlocale(locale.LC_ALL, str(locale_))
break # break if it is successful
except locale.Error:
pass
else:
@ -142,10 +176,21 @@ 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):
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')")
feed_keys = ['FEED_ATOM', 'FEED_RSS',
'FEED_ALL_ATOM', 'FEED_ALL_RSS',
'CATEGORY_FEED_ATOM', 'CATEGORY_FEED_RSS',
'TAG_FEED_ATOM', 'TAG_FEED_RSS',
'TRANSLATION_FEED_ATOM', 'TRANSLATION_FEED_RSS',
]
if any(settings.get(k) for k in feed_keys):
if not settings.get('FEED_DOMAIN'):
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')")
if not settings.get('SITEURL'):
logger.warn("Feeds generated without SITEURL set properly may not be valid")
if not 'TIMEZONE' in settings:
logger.warn("No timezone information specified in the settings. Assuming"
@ -153,12 +198,23 @@ def configure_settings(settings, default_settings=None, filename=None):
"http://docs.notmyidea.org/alexis/pelican/settings.html#timezone "
"for more information")
if 'WEBASSETS' in settings and settings['WEBASSETS'] is not False:
try:
from webassets.ext.jinja2 import AssetsExtension
settings['JINJA_EXTENSIONS'].append(AssetsExtension)
except ImportError:
logger.warn("You must install the webassets module to use WEBASSETS.")
settings['WEBASSETS'] = False
if 'LESS_GENERATOR' in settings:
logger.warn("The LESS_GENERATOR setting has been removed in favor "
"of the Webassets plugin")
if 'OUTPUT_SOURCES_EXTENSION' in settings:
if not isinstance(settings['OUTPUT_SOURCES_EXTENSION'], six.string_types):
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'])
filename_metadata = settings.get('FILENAME_METADATA')
if filename_metadata and not isinstance(filename_metadata, six.string_types):
logger.error("Detected misconfiguration with FILENAME_METADATA"
" setting (must be string or compiled pattern), falling"
"back to the default")
settings['FILENAME_METADATA'] = \
_DEFAULT_CONFIG['FILENAME_METADATA']
return settings

View file

@ -1,5 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
from blinker import signal
initialized = signal('pelican_initialized')
finalized = signal('pelican_finalized')
article_generate_preread = signal('article_generate_preread')
generator_init = signal('generator_init')
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,11 @@ 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*='github.com'],
.social a[href*='git.io'] {background-image: url('../images/icons/github.png');}
.social a[href*='gittip.com'] {background-image: url('../images/icons/gittip.png');}
.social a[href*='plus.google.com'] {background-image: url('../images/icons/google-plus.png');}
/*
About

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 963 B

After

Width:  |  Height:  |  Size: 958 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 B

After

Width:  |  Height:  |  Size: 202 B

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 227 B

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 980 B

After

Width:  |  Height:  |  Size: 975 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 B

After

Width:  |  Height:  |  Size: 896 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 896 B

After

Width:  |  Height:  |  Size: 879 B

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 835 B

After

Width:  |  Height:  |  Size: 830 B

Before After
Before After

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

@ -5,7 +5,7 @@
<article>
<header>
<h1 class="entry-title">
<a href="{{ article.url }}" rel="bookmark"
<a href="{{ SITEURL }}/{{ article.url }}" rel="bookmark"
title="Permalink to {{ article.title|striptags }}">{{ article.title}}</a></h1>
{% include 'twitter.html' %}
</header>

View file

@ -10,5 +10,6 @@
{% endif %}
<p>In <a href="{{ SITEURL }}/{{ article.category.url }}">{{ article.category }}</a>. {% if PDF_PROCESSOR %}<a href="{{ SITEURL }}/pdf/{{ article.slug }}.pdf">get the pdf</a>{% endif %}</p>
{% include 'taglist.html' %}
{% include 'translations.html' %}
{% import 'translations.html' as translations with context %}
{{ translations.translations_for(article) }}
</footer><!-- /.post-info -->

View file

@ -4,9 +4,11 @@
<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_RSS %}
<link href="{{ FEED_DOMAIN }}/{{ FEED_RSS }}" type="application/rss+xml" rel="alternate" title="{{ SITENAME }} RSS Feed" />
{% if FEED_ALL_ATOM %}
<link href="{{ FEED_DOMAIN }}/{{ FEED_ALL_ATOM }}" type="application/atom+xml" rel="alternate" title="{{ SITENAME }} Atom Feed" />
{% endif %}
{% if FEED_ALL_RSS %}
<link href="{{ FEED_DOMAIN }}/{{ FEED_ALL_RSS }}" type="application/rss+xml" rel="alternate" title="{{ SITENAME }} RSS Feed" />
{% endif %}
<!--[if IE]>
@ -24,7 +26,7 @@
<body id="index" class="home">
{% include 'github.html' %}
<header id="banner" class="body">
<h1><a href="{{ SITEURL }}">{{ SITENAME }} {% if SITESUBTITLE %} <strong>{{ SITESUBTITLE }}</strong>{% endif %}</a></h1>
<h1><a href="{{ SITEURL }}/">{{ SITENAME }} {% if SITESUBTITLE %} <strong>{{ SITESUBTITLE }}</strong>{% endif %}</a></h1>
<nav><ul>
{% for title, link in MENUITEMS %}
<li><a href="{{ link }}">{{ title }}</a></li>
@ -56,9 +58,9 @@
<div class="social">
<h2>social</h2>
<ul>
<li><a href="{{ FEED_DOMAIN }}/{{ FEED }}" 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>
<li><a href="{{ FEED_DOMAIN }}/{{ FEED_ALL_ATOM }}" type="application/atom+xml" rel="alternate">atom feed</a></li>
{% if FEED_ALL_RSS %}
<li><a href="{{ FEED_DOMAIN }}/{{ FEED_ALL_RSS }}" type="application/rss+xml" rel="alternate">rss feed</a></li>
{% endif %}
{% for name, link in SOCIAL %}
@ -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

@ -3,6 +3,8 @@
{% block content %}
<section id="content" class="body">
<h1 class="entry-title">{{ page.title }}</h1>
{% import 'translations.html' as translations with context %}
{{ translations.translations_for(page) }}
{% if PDF_PROCESSOR %}<a href="{{ SITEURL }}/pdf/{{ page.slug }}.pdf">get
the pdf</a>{% endif %}
{{ page.content }}

View file

@ -1,6 +1,8 @@
{% macro translations_for(article) %}
{% if article.translations %}
Translations:
{% for translation in article.translations %}
<a href="{{ SITEURL }}/{{ translation.url }}">{{ translation.lang }}</a>
{% endfor %}
{% endif %}
{% endmacro %}

View file

@ -5,6 +5,8 @@
<h2 class="entry-title">
<a href="{{ article.url }}" rel="bookmark"
title="Permalink to {{ article.title|striptags }}">{{ article.title }}</a></h2>
{% import 'translations.html' as translations with context %}
{{ translations.translations_for(article) }}
</header>
<footer class="post-info">
<abbr class="published" title="{{ article.date.isoformat() }}">

View file

@ -4,6 +4,30 @@
{% block head %}
<title>{% block title %}{{ SITENAME }}{% endblock title %}</title>
<meta charset="utf-8" />
{% if FEED_ALL_ATOM %}
<link href="{{ FEED_DOMAIN }}/{{ FEED_ALL_ATOM }}" type="application/atom+xml" rel="alternate" title="{{ SITENAME }} Full Atom Feed" />
{% endif %}
{% if FEED_ALL_RSS %}
<link href="{{ FEED_DOMAIN }}/{{ FEED_ALL_RSS }}" type="application/rss+xml" rel="alternate" title="{{ SITENAME }} Full RSS Feed" />
{% endif %}
{% 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 +53,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

@ -1,14 +1,14 @@
{% extends "base.html" %}
{% block content %}
{% block content %}
<section id="content">
{% block content_title %}
<h2>All articles</h2>
{% endblock %}
<ol id="post-list">
{% for article in articles_page.object_list %}
<li><article class="hentry">
<header> <h2 class="entry-title"><a href="{{ SITEURL }}/{{ article.url }}" rel="bookmark" title="Permalink to {{ article.title}}">{{ article.title }}</a></h2> </header>
{% for article in articles_page.object_list %}
<li><article class="hentry">
<header> <h2 class="entry-title"><a href="{{ SITEURL }}/{{ article.url }}" rel="bookmark" title="Permalink to {{ article.title|striptags }}">{{ article.title }}</a></h2> </header>
<footer class="post-info">
<abbr class="published" title="{{ article.date.isoformat() }}"> {{ article.locale_date }} </abbr>
{% if article.author %}<address class="vcard author">By <a class="url fn" href="{{ SITEURL }}/{{ article.author.url }}">{{ article.author }}</a></address>{% endif %}

View file

@ -2,5 +2,8 @@
{% block title %}{{ page.title }}{%endblock%}
{% block content %}
<h1>{{ page.title }}</h1>
{% import 'translations.html' as translations with context %}
{{ translations.translations_for(page) }}
{{ page.content }}
{% endblock %}

View file

@ -0,0 +1,9 @@
{% macro translations_for(article) %}
{% if article.translations %}
Translations:
{% for translation in article.translations %}
<a href="{{ SITEURL }}/{{ translation.url }}">{{ translation.lang }}</a>
{% endfor %}
{% endif %}
{% endmacro %}

View file

@ -1,6 +1,14 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
import argparse
try:
# py3k import
from html.parser import HTMLParser
except ImportError:
# py2 import
from HTMLParser import HTMLParser # NOQA
import os
import subprocess
import sys
@ -14,14 +22,14 @@ from pelican.utils import slugify
def wp2fields(xml):
"""Opens a wordpress XML file, and yield pelican fields"""
try:
from BeautifulSoup import BeautifulStoneSoup
from bs4 import BeautifulSoup
except ImportError:
error = ('Missing dependency '
'"BeautifulSoup" required to import Wordpress XML files.')
'"BeautifulSoup4" and "lxml" required to import Wordpress XML files.')
sys.exit(error)
xmlfile = open(xml, encoding='utf-8').read()
soup = BeautifulStoneSoup(xmlfile)
soup = BeautifulSoup(xmlfile, "xml")
items = soup.rss.channel.findAll('item')
for item in items:
@ -29,7 +37,8 @@ def wp2fields(xml):
if item.fetch('wp:status')[0].contents[0] == "publish":
try:
title = item.title.contents[0]
# Use HTMLParser due to issues with BeautifulSoup 3
title = HTMLParser().unescape(item.title.contents[0])
except IndexError:
continue
@ -52,10 +61,10 @@ def wp2fields(xml):
def dc2fields(file):
"""Opens a Dotclear export file, and yield pelican fields"""
try:
from BeautifulSoup import BeautifulStoneSoup
from bs4 import BeautifulSoup
except ImportError:
error = ('Missing dependency '
'"BeautifulSoup" required to import Dotclear files.')
'"BeautifulSoup4" and "lxml" required to import Dotclear files.')
sys.exit(error)
@ -140,13 +149,27 @@ def dc2fields(file):
if len(tag) > 1:
if int(tag[:1]) == 1:
newtag = tag.split('"')[1]
tags.append(unicode(BeautifulStoneSoup(newtag,convertEntities=BeautifulStoneSoup.HTML_ENTITIES )))
tags.append(
BeautifulSoup(
newtag
, "xml"
)
# bs4 always outputs UTF-8
.decode('utf-8')
)
else:
i=1
j=1
while(i <= int(tag[:1])):
newtag = tag.split('"')[j].replace('\\','')
tags.append(unicode(BeautifulStoneSoup(newtag,convertEntities=BeautifulStoneSoup.HTML_ENTITIES )))
tags.append(
BeautifulSoup(
newtag
, "xml"
)
# bs4 always outputs UTF-8
.decode('utf-8')
)
i=i+1
if j < int(tag[:1])*2:
j=j+2
@ -179,44 +202,53 @@ def feed2fields(file):
yield (entry.title, entry.description, slug, date, author, [], tags, "html")
def build_header(title, date, author, categories, tags):
def build_header(title, date, author, categories, tags, slug):
"""Build a header from a list of fields"""
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:
header += ':tags: %s\n' % ', '.join(tags)
if slug:
header += ':slug: %s\n' % slug
header += '\n'
return header
def build_markdown_header(title, date, author, categories, tags):
def build_markdown_header(title, date, author, categories, tags, slug):
"""Build a header from a list of fields"""
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:
header += 'Tags: %s\n' % ', '.join(tags)
if slug:
header += 'Slug: %s\n' % slug
header += '\n'
return header
def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=False):
def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=False, disable_slugs=False):
for title, content, filename, date, author, categories, tags, in_markup in fields:
slug = not disable_slugs and filename or None
if (in_markup == "markdown") or (out_markup == "markdown") :
ext = '.md'
header = build_markdown_header(title, date, author, categories, tags)
header = build_markdown_header(title, date, author, categories, tags, slug)
else:
out_markup = "rst"
ext = '.rst'
header = build_header(title, date, author, categories, tags)
header = build_header(title, date, author, categories, tags, slug)
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)):
@ -232,8 +264,8 @@ def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=Fals
with open(html_filename, 'w', encoding='utf-8') as fp:
# 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 = content.splitlines()
paragraphs = ['<p>{0}</p>'.format(p) for p in paragraphs]
new_content = ''.join(paragraphs)
fp.write(new_content)
@ -253,7 +285,7 @@ def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=Fals
elif rc > 0:
error = "Please, check your Pandoc installation."
exit(error)
except OSError, e:
except OSError as e:
error = "Pandoc execution failed: %s" % e
exit(error)
@ -272,8 +304,8 @@ def fields2pelican(fields, out_markup, output_path, dircat=False, strip_raw=Fals
def main():
parser = argparse.ArgumentParser(
description="Transform feed, Wordpress or Dotclear files to rst files."
"Be sure to have pandoc installed",
description="Transform feed, Wordpress or Dotclear files to reST (rst) "
"or Markdown (md) files. Be sure to have pandoc installed.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(dest='input', help='The input file to read')
@ -292,6 +324,11 @@ def main():
parser.add_argument('--strip-raw', action='store_true', dest='strip_raw',
help="Strip raw HTML code that can't be converted to "
"markup such as flash embeds or iframes (wordpress import only)")
parser.add_argument('--disable-slugs', action='store_true',
dest='disable_slugs',
help='Disable storing slugs from imported posts within output. '
'With this disabled, your Pelican URLs may not be consistent '
'with your original posts.')
args = parser.parse_args()
@ -322,4 +359,5 @@ def main():
fields2pelican(fields, args.markup, args.output,
dircat=args.dircat or False,
strip_raw=args.strip_raw or False)
strip_raw=args.strip_raw or False,
disable_slugs=args.disable_slugs or False)

View file

@ -1,19 +1,23 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- #
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
import six
import os
import string
import argparse
import sys
import codecs
from pelican import __version__
_TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), \
_TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"templates")
CONF = {
'pelican' : 'pelican',
'pelicanopts' : '',
'pelican': 'pelican',
'pelicanopts': '',
'basedir': '.',
'ftp_host': 'localhost',
'ftp_user': 'anonymous',
@ -22,20 +26,41 @@ CONF = {
'ssh_port': 22,
'ssh_user': 'root',
'ssh_target_dir': '/var/www',
'dropbox_dir' : '~/Dropbox/Public/',
'default_pagination' : 10,
'dropbox_dir': '~/Dropbox/Public/',
'default_pagination': 10,
'siteurl': '',
'lang': 'en'
}
def _input_compat(prompt):
if six.PY3:
r = input(prompt)
else:
r = raw_input(prompt).decode('utf-8')
return r
def get_template(name):
if six.PY3:
str_compat = str
else:
str_compat = unicode
def decoding_strings(f):
def wrapper(*args, **kwargs):
out = f(*args, **kwargs)
if isinstance(out, six.string_types):
# todo: make encoding configurable?
return out.decode(sys.stdin.encoding)
return out
return wrapper
def get_template(name, as_encoding='utf-8'):
template = os.path.join(_TEMPLATES_DIR, "{0}.in".format(name))
if not os.path.isfile(template):
raise RuntimeError("Cannot open {0}".format(template))
with open(template, 'r') as fd:
with codecs.open(template, 'r', as_encoding) as fd:
line = fd.readline()
while line:
yield line
@ -43,14 +68,15 @@ def get_template(name):
fd.close()
def ask(question, answer=str, default=None, l=None):
if answer == str:
@decoding_strings
def ask(question, answer=str_compat, default=None, l=None):
if answer == str_compat:
r = ''
while True:
if default:
r = raw_input('> {0} [{1}] '.format(question, default))
r = _input_compat('> {0} [{1}] '.format(question, default))
else:
r = raw_input('> {0} '.format(question, default))
r = _input_compat('> {0} '.format(question, default))
r = r.strip()
@ -64,7 +90,7 @@ def ask(question, answer=str, default=None, l=None):
if l and len(r) != l:
print('You must enter a {0} letters long string'.format(l))
else:
break
break
return r
@ -72,11 +98,11 @@ def ask(question, answer=str, default=None, l=None):
r = None
while True:
if default is True:
r = raw_input('> {0} (Y/n) '.format(question))
r = _input_compat('> {0} (Y/n) '.format(question))
elif default is False:
r = raw_input('> {0} (y/N) '.format(question))
r = _input_compat('> {0} (y/N) '.format(question))
else:
r = raw_input('> {0} (y/n) '.format(question))
r = _input_compat('> {0} (y/n) '.format(question))
r = r.strip().lower()
@ -96,9 +122,9 @@ def ask(question, answer=str, default=None, l=None):
r = None
while True:
if default:
r = raw_input('> {0} [{1}] '.format(question, default))
r = _input_compat('> {0} [{1}] '.format(question, default))
else:
r = raw_input('> {0} '.format(question))
r = _input_compat('> {0} '.format(question))
r = r.strip()
@ -113,7 +139,7 @@ def ask(question, answer=str, default=None, l=None):
print('You must enter an integer')
return r
else:
raise NotImplemented('Argument `answer` must be str, bool, or integer')
raise NotImplemented('Argument `answer` must be str_compat, bool, or integer')
def main():
@ -135,23 +161,25 @@ def main():
This script will help you create a new Pelican-based website.
Please answer the following questions so this script can generate the files needed by Pelican.
Please answer the following questions so this script can generate the files
needed by Pelican.
'''.format(v=__version__))
project = os.path.join(os.environ['VIRTUAL_ENV'], '.project')
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'])
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['basedir'] = os.path.abspath(ask('Where do you want to create your new web site?', answer=str_compat, 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)
CONF['sitename'] = ask('What will be the title of this web site?', answer=str_compat, default=args.title)
CONF['author'] = ask('Who will be the author of this web site?', answer=str_compat, default=args.author)
CONF['lang'] = ask('What will be the default language of this web site?', str_compat, 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['siteurl'] = ask('What is your URL prefix? (see above example; no trailing slash)', str_compat, CONF['siteurl'])
CONF['with_pagination'] = ask('Do you want to enable article pagination?', bool, bool(CONF['default_pagination']))
@ -161,57 +189,77 @@ Please answer the following questions so this script can generate the files need
CONF['default_pagination'] = False
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 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'])
CONF['ftp_host'] = ask('What is the hostname of your FTP server?', str_compat, CONF['ftp_host'])
CONF['ftp_user'] = ask('What is your username on that server?', str_compat, CONF['ftp_user'])
CONF['ftp_target_dir'] = ask('Where do you want to put your web site on that server?', str_compat, 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_host'] = ask('What is the hostname of your SSH server?', str_compat, 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'])
CONF['ssh_user'] = ask('What is your username on that server?', str_compat, CONF['ssh_user'])
CONF['ssh_target_dir'] = ask('Where do you want to put your web site on that server?', str_compat, 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'])
CONF['dropbox_dir'] = ask('Where is your Dropbox directory?', str_compat, CONF['dropbox_dir'])
try:
os.makedirs(os.path.join(CONF['basedir'], 'content'))
except OSError, e:
except OSError as e:
print('Error: {0}'.format(e))
try:
os.makedirs(os.path.join(CONF['basedir'], 'output'))
except OSError, e:
except OSError as e:
print('Error: {0}'.format(e))
try:
with open(os.path.join(CONF['basedir'], 'pelicanconf.py'), 'w') as fd:
with codecs.open(os.path.join(CONF['basedir'], 'pelicanconf.py'), 'w', 'utf-8') as fd:
conf_python = dict()
for key, value in CONF.items():
conf_python[key] = repr(value)
for line in get_template('pelicanconf.py'):
template = string.Template(line)
fd.write(template.safe_substitute(CONF))
fd.write(template.safe_substitute(conf_python))
fd.close()
except OSError, e:
except OSError as e:
print('Error: {0}'.format(e))
try:
with open(os.path.join(CONF['basedir'], 'publishconf.py'), 'w') as fd:
with codecs.open(os.path.join(CONF['basedir'], 'publishconf.py'), 'w', 'utf-8') as fd:
for line in get_template('publishconf.py'):
template = string.Template(line)
fd.write(template.safe_substitute(CONF))
fd.close()
except OSError, e:
except OSError as e:
print('Error: {0}'.format(e))
if mkfile:
try:
with open(os.path.join(CONF['basedir'], 'Makefile'), 'w') as fd:
with codecs.open(os.path.join(CONF['basedir'], 'Makefile'), 'w', 'utf-8') as fd:
for line in get_template('Makefile'):
template = string.Template(line)
fd.write(template.safe_substitute(CONF))
fd.close()
except OSError, e:
except OSError as e:
print('Error: {0}'.format(e))
if develop:
conf_shell = dict()
for key, value in CONF.items():
if isinstance(value, six.string_types) and ' ' in value:
value = '"' + value.replace('"', '\\"') + '"'
conf_shell[key] = value
try:
with codecs.open(os.path.join(CONF['basedir'], 'develop_server.sh'), 'w', 'utf-8') 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')), 493) # mode 0o755
except OSError as e:
print('Error: {0}'.format(e))
print('Done. Your new project is available at %s' % CONF['basedir'])

View file

@ -1,5 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
import six
import argparse
import os
@ -28,7 +31,7 @@ _BUILTIN_THEMES = ['simple', 'notmyidea']
def err(msg, die=None):
"""Print an error message and exits if an exit code is given"""
sys.stderr.write(str(msg) + '\n')
sys.stderr.write(msg + '\n')
if die:
sys.exit((die if type(die) is int else 1))
@ -180,7 +183,19 @@ def install(path, v=False, u=False):
print("Copying `{p}' to `{t}' ...".format(p=path, t=theme_path))
try:
shutil.copytree(path, theme_path)
except Exception, e:
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, 493) # 0o755
for f in files:
fname = os.path.join(root, f)
os.chmod(fname, 420) # 0o644
except OSError as e:
err("Cannot change permissions of files or directory in `{r}':\n{e}".format(r=theme_path, e=str(e)), die=False)
except Exception as e:
err("Cannot copy `{p}' to `{t}':\n{e}".format(p=path, t=theme_path, e=str(e)))
@ -200,7 +215,7 @@ def symlink(path, v=False):
print("Linking `{p}' to `{t}' ...".format(p=path, t=theme_path))
try:
os.symlink(path, theme_path)
except Exception, e:
except Exception as e:
err("Cannot link `{p}' to `{t}':\n{e}".format(p=path, t=theme_path, e=str(e)))
@ -221,7 +236,7 @@ def clean(v=False):
print('Removing {0}'.format(path))
try:
os.remove(path)
except OSError, e:
except OSError as e:
print('Error: cannot remove {0}'.format(path))
else:
c+=1

View file

@ -1,7 +1,7 @@
PELICAN=$pelican
PELICANOPTS=$pelicanopts
BASEDIR=$$(PWD)
BASEDIR=$$(CURDIR)
INPUTDIR=$$(BASEDIR)/content
OUTPUTDIR=$$(BASEDIR)/output
CONFFILE=$$(BASEDIR)/pelicanconf.py
@ -24,11 +24,15 @@ help:
@echo 'Usage: '
@echo ' make html (re)generate the web site '
@echo ' make clean remove the generated files '
@echo ' make regenerate regenerate files upon modification '
@echo ' make publish generate using production settings '
@echo ' ftp_upload upload the web site via FTP '
@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 ' rsync_upload upload the web site via rsync/ssh '
@echo ' ftp_upload upload the web site via FTP '
@echo ' github upload the web site via gh-pages '
@echo ' '
@ -45,19 +49,22 @@ regenerate: clean
$$(PELICAN) -r $$(INPUTDIR) -o $$(OUTPUTDIR) -s $$(CONFFILE) $$(PELICANOPTS)
serve:
cd $$(OUTPUTDIR) && python -m SimpleHTTPServer
cd $$(OUTPUTDIR) && python -m pelican.server
devserver:
$$(BASEDIR)/develop_server.sh restart
publish:
$$(PELICAN) $$(INPUTDIR) -o $$(OUTPUTDIR) -s $$(PUBLISHCONF) $$(PELICANOPTS)
dropbox_upload: publish
cp -r $$(OUTPUTDIR)/* $$(DROPBOX_DIR)
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)
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)
ftp_upload: publish
lftp ftp://$$(FTP_USER)@$$(FTP_HOST) -e "mirror -R $$(OUTPUTDIR) $$(FTP_TARGET_DIR) ; quit"
@ -66,4 +73,4 @@ github: publish
ghp-import $$(OUTPUTDIR)
git push origin gh-pages
.PHONY: html help clean regenerate serve publish 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 pelican.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 != "" ]]; then
echo "Killing pelican.server"
kill $$PID
else
echo "Stale PID, deleting"
fi
rm $$SRV_PID
else
echo "pelican.server 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 pelican.server"
shift
$$PELICAN --debug --autoreload -r $$INPUTDIR -o $$OUTPUTDIR -s $$CONFFILE $$PELICANOPTS &
echo $$! > $$PELICAN_PID
cd $$OUTPUTDIR
python -m pelican.server &
echo $$! > $$SRV_PID
cd $$BASEDIR
sleep 1 && echo 'Pelican and pelican.server 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,13 +1,13 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- #
AUTHOR = u"$author"
SITENAME = u"$sitename"
AUTHOR = $author
SITENAME = $sitename
SITEURL = ''
TIMEZONE = 'Europe/Paris'
DEFAULT_LANG = '$lang'
DEFAULT_LANG = $lang
# Blogroll
LINKS = (('Pelican', 'http://docs.notmyidea.org/alexis/pelican/'),

View file

@ -1,6 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- #
import sys
sys.path.append('.')
from pelicanconf import *
SITEURL = '$siteurl'

View file

@ -1,12 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function
import six
import os
import re
import pytz
import shutil
import traceback
import logging
from collections import defaultdict
import errno
import locale
import fnmatch
from collections import defaultdict, Hashable
from functools import partial
from codecs import open as _open
from codecs import open
from datetime import datetime
from itertools import groupby
from jinja2 import Markup
@ -15,6 +23,149 @@ from operator import attrgetter
logger = logging.getLogger(__name__)
def strftime(date, date_format):
"""
Replacement for the builtin strftime().
This :func:`strftime()` is compatible to Python 2 and 3. In both cases,
input and output is always unicode.
Still, Python 3's :func:`strftime()` seems to somehow "normalize" unicode
chars in the format string. So if e.g. your format string contains 'ø' or
'ä', the result will be 'o' and 'a'.
See here for an `extensive testcase <https://github.com/dmdm/test_strftime>`_.
:param date: Any object that sports a :meth:`strftime()` method.
:param date_format: Format string, can always be unicode.
:returns: Unicode string with formatted date.
"""
# As tehkonst confirmed, above mentioned testcase runs correctly on
# Python 2 and 3 on Windows as well. Thanks.
if six.PY3:
# It could be so easy... *sigh*
return date.strftime(date_format)
# TODO Perhaps we should refactor again, so that the
# xmlcharrefreplace-regex-dance is always done, regardless
# of the Python version.
else:
# We must ensure that the format string is an encoded byte
# string, ASCII only WTF!!!
# But with "xmlcharrefreplace" our formatted date will produce
# *yuck* like this:
# "Øl trinken beim Besäufnis"
# --> "&#216;l trinken beim Bes&#228;ufnis"
date_format = date_format.encode('ascii',
errors="xmlcharrefreplace")
result = date.strftime(date_format)
# strftime() returns an encoded byte string
# which we must decode into unicode.
lang_code, enc = locale.getlocale(locale.LC_ALL)
if enc:
result = result.decode(enc)
else:
result = unicode(result)
# Convert XML character references back to unicode characters.
if "&#" in result:
result = re.sub(r'&#(?P<num>\d+);'
, lambda m: unichr(int(m.group('num')))
, result
)
return result
#----------------------------------------------------------------------------
# Stolen from Django: django.utils.encoding
#
def python_2_unicode_compatible(klass):
"""
A decorator that defines __unicode__ and __str__ methods under Python 2.
Under Python 3 it does nothing.
To support Python 2 and 3 with a single code base, define a __str__ method
returning text and apply this decorator to the class.
"""
if not six.PY3:
klass.__unicode__ = klass.__str__
klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
return klass
#----------------------------------------------------------------------------
class NoFilesError(Exception):
pass
class memoized(object):
'''Decorator. Caches a function's return value each time it is called.
If called later with the same arguments, the cached value is returned
(not reevaluated).
'''
def __init__(self, func):
self.func = func
self.cache = {}
def __call__(self, *args):
if not isinstance(args, Hashable):
# uncacheable. a list, for instance.
# better to not cache than blow up.
return self.func(*args)
if args in self.cache:
return self.cache[args]
else:
value = self.func(*args)
self.cache[args] = value
return value
def __repr__(self):
'''Return the function's docstring.'''
return self.func.__doc__
def __get__(self, obj, objtype):
'''Support instance methods.'''
return partial(self.__call__, obj)
def deprecated_attribute(old, new, since=None, remove=None, doc=None):
"""Attribute deprecation decorator for gentle upgrades
For example:
class MyClass (object):
@deprecated_attribute(
old='abc', new='xyz', since=(3, 2, 0), remove=(4, 1, 3))
def abc(): return None
def __init__(self):
xyz = 5
Note that the decorator needs a dummy method to attach to, but the
content of the dummy method is ignored.
"""
def _warn():
version = '.'.join(six.text_type(x) for x in since)
message = ['{} has been deprecated since {}'.format(old, version)]
if remove:
version = '.'.join(six.text_type(x) for x in remove)
message.append(
' and will be removed by version {}'.format(version))
message.append('. Use {} instead.'.format(new))
logger.warning(''.join(message))
logger.debug(''.join(
six.text_type(x) for x in traceback.format_stack()))
def fget(self):
_warn()
return getattr(self, new)
def fset(self, value):
_warn()
setattr(self, new, value)
def decorator(dummy):
return property(fget=fget, fset=fset, doc=doc)
return decorator
def get_date(string):
"""Return a datetime object from a string.
@ -34,12 +185,13 @@ def get_date(string):
raise ValueError("'%s' is not a valid date" % string)
class open(object):
class pelican_open(object):
"""Open a file and return it's content"""
def __init__(self, filename):
self.filename = filename
def __enter__(self):
return _open(self.filename, encoding='utf-8').read()
return open(self.filename, encoding='utf-8').read()
def __exit__(self, exc_type, exc_value, traceback):
pass
@ -51,12 +203,24 @@ def slugify(value):
Took from django sources.
"""
# TODO Maybe steal again from current Django 1.5dev
value = Markup(value).striptags()
if type(value) == unicode:
import unicodedata
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
return re.sub('[-\s]+', '-', value)
# value must be unicode per se
import unicodedata
from unidecode import unidecode
# unidecode returns str in Py2 and 3, so in Py2 we have to make
# it unicode again
value = unidecode(value)
if isinstance(value, six.binary_type):
value = value.decode('ascii')
# still unicode
value = unicodedata.normalize('NFKD', value)
value = re.sub('[^\w\s-]', '', value).strip().lower()
value = re.sub('[-\s]+', '-', value)
# we want only ASCII chars
value = value.encode('ascii', 'ignore')
# but Pelican should generally use only unicode
return value.decode('ascii')
def copy(path, source, destination, destination_path=None, overwrite=False):
@ -86,16 +250,32 @@ def copy(path, source, destination, destination_path=None, overwrite=False):
if overwrite:
shutil.rmtree(destination_)
shutil.copytree(source_, destination_)
logger.info('replacement of %s with %s' % (source_, destination_))
logger.info('replacement of %s with %s' % (source_,
destination_))
elif os.path.isfile(source_):
dest_dir = os.path.dirname(destination_)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
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 as e:
logger.error("Unable to delete file %s; %e" % path, e)
return
# remove all the existing content from the output folder
for filename in os.listdir(path):
file = os.path.join(path, filename)
@ -103,21 +283,25 @@ def clean_output_dir(path):
try:
shutil.rmtree(file)
logger.debug("Deleted directory %s" % file)
except Exception, e:
except Exception as 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:
except Exception as 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):
"""Return the relative path to the given filename"""
return '../' * filename.count('/') + '.'
def get_relative_path(path):
"""Return the relative path from the given path to the root path."""
nslashes = path.count('/')
if nslashes == 0:
return '.'
else:
return '/'.join(['..'] * nslashes)
def truncate_html_words(s, num, end_text='...'):
@ -131,7 +315,7 @@ def truncate_html_words(s, num, end_text='...'):
"""
length = int(num)
if length <= 0:
return u''
return ''
html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area',
'hr', 'input')
@ -205,34 +389,31 @@ def process_translations(content_list):
for slug, items in grouped_by_slugs:
items = list(items)
# find items with default language
default_lang_items = filter(attrgetter('in_default_lang'), items)
default_lang_items = list(filter(attrgetter('in_default_lang'), items))
len_ = len(default_lang_items)
if len_ > 1:
logger.warning(u'there are %s variants of "%s"' % (len_, slug))
logger.warning('there are %s variants of "%s"' % (len_, slug))
for x in default_lang_items:
logger.warning(' %s' % x.filename)
logger.warning(' {}'.format(x.source_path))
elif len_ == 0:
default_lang_items = items[:1]
if not slug:
msg = 'empty slug for %r. ' % default_lang_items[0].filename\
+ 'You can fix this by adding a title or a slug to your '\
+ 'content'
logger.warning(msg)
logger.warning((
'empty slug for {!r}. '
'You can fix this by adding a title or a slug to your '
'content'
).format(default_lang_items[0].source_path))
index.extend(default_lang_items)
translations.extend(filter(
lambda x: x not in default_lang_items,
items
))
translations.extend([x for x in items if x not in default_lang_items])
for a in items:
a.translations = filter(lambda x: x != a, items)
a.translations = [x for x in items if x != a]
return index, translations
LAST_MTIME = 0
def files_changed(path, extensions):
def files_changed(path, extensions, ignores=[]):
"""Return True if the files have changed since the last check"""
def file_times(path):
@ -240,28 +421,32 @@ def files_changed(path, extensions):
for root, dirs, files in os.walk(path):
dirs[:] = [x for x in dirs if x[0] != '.']
for f in files:
if any(f.endswith(ext) for ext in extensions):
if any(f.endswith(ext) for ext in extensions) \
and not any(fnmatch.fnmatch(f, ignore) for ignore in ignores):
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
FILENAMES_MTIMES = defaultdict(int)
def file_changed(filename):
mtime = os.stat(filename).st_mtime
if FILENAMES_MTIMES[filename] == 0:
FILENAMES_MTIMES[filename] = mtime
def file_changed(path):
mtime = os.stat(path).st_mtime
if FILENAMES_MTIMES[path] == 0:
FILENAMES_MTIMES[path] = mtime
return False
else:
if mtime > FILENAMES_MTIMES[filename]:
FILENAMES_MTIMES[filename] = mtime
if mtime > FILENAMES_MTIMES[path]:
FILENAMES_MTIMES[path] = mtime
return True
return False
@ -276,3 +461,11 @@ def set_date_tzinfo(d, tz_name=None):
return tz.localize(d)
else:
return d
def mkdir_p(path):
try:
os.makedirs(path)
except OSError as e:
if e.errno != errno.EEXIST or not os.path.isdir(path):
raise

View file

@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
from __future__ import with_statement
from __future__ import with_statement, unicode_literals, print_function
import six
import os
import re
import locale
import logging
from codecs import open
from functools import partial
from feedgenerator import Atom1Feed, Rss201rev2Feed
from jinja2 import Markup
from pelican.paginator import Paginator
@ -41,45 +40,45 @@ class Writer(object):
link='%s/%s' % (self.site_url, item.url),
unique_id='tag:%s,%s:%s' % (self.site_url.replace('http://', ''),
item.date.date(), item.url),
description=item.content,
description=item.get_content(self.site_url),
categories=item.tags if hasattr(item, 'tags') else None,
author_name=getattr(item, 'author', 'John Doe'),
author_name=getattr(item, 'author', ''),
pubdate=set_date_tzinfo(item.date,
self.settings.get('TIMEZONE', None)))
def write_feed(self, elements, context, filename=None, feed_type='atom'):
def write_feed(self, elements, context, path=None, feed_type='atom'):
"""Generate a feed with the list of articles provided
Return the feed. If no output_path or filename is specified, just
Return the feed. If no path or output_path is specified, just
return the feed object.
:param elements: the articles to put on the feed.
:param context: the context to get the feed metadata.
:param filename: the filename to output.
:param path: the path to output.
:param feed_type: the feed type to use (atom or rss)
"""
old_locale = locale.setlocale(locale.LC_ALL)
locale.setlocale(locale.LC_ALL, 'C')
locale.setlocale(locale.LC_ALL, str('C'))
try:
self.site_url = context.get('SITEURL', get_relative_path(filename))
self.site_url = context.get('SITEURL', get_relative_path(path))
self.feed_domain = context.get('FEED_DOMAIN')
self.feed_url = '%s/%s' % (self.feed_domain, filename)
self.feed_url = '{}/{}'.format(self.feed_domain, path)
feed = self._create_new_feed(feed_type, context)
max_items = len(elements)
if self.settings['FEED_MAX_ITEMS']:
max_items = min(self.settings['FEED_MAX_ITEMS'], max_items)
for i in xrange(max_items):
for i in range(max_items):
self._add_item_to_the_feed(feed, elements[i])
if filename:
complete_path = os.path.join(self.output_path, filename)
if path:
complete_path = os.path.join(self.output_path, path)
try:
os.makedirs(os.path.dirname(complete_path))
except Exception:
pass
fp = open(complete_path, 'w')
fp = open(complete_path, 'w', encoding='utf-8' if six.PY3 else None)
feed.write(fp, 'utf-8')
logger.info('writing %s' % complete_path)
@ -110,34 +109,34 @@ class Writer(object):
def _write_file(template, localcontext, output_path, name):
"""Render the template write the file."""
old_locale = locale.setlocale(locale.LC_ALL)
locale.setlocale(locale.LC_ALL, 'C')
locale.setlocale(locale.LC_ALL, str('C'))
try:
output = template.render(localcontext)
finally:
locale.setlocale(locale.LC_ALL, old_locale)
filename = os.sep.join((output_path, name))
path = os.path.join(output_path, name)
try:
os.makedirs(os.path.dirname(filename))
os.makedirs(os.path.dirname(path))
except Exception:
pass
with open(filename, 'w', encoding='utf-8') as f:
with open(path, 'w', encoding='utf-8') as f:
f.write(output)
logger.info(u'writing %s' % filename)
logger.info('writing {}'.format(path))
localcontext = context.copy()
if relative_urls:
localcontext['SITEURL'] = get_relative_path(name)
relative_path = get_relative_path(name)
context['localsiteurl'] = relative_path
localcontext['SITEURL'] = relative_path
localcontext.update(kwargs)
if relative_urls:
self.update_context_contents(name, localcontext)
# check paginated
paginated = paginated or {}
if paginated:
# pagination needed, init paginators
paginators = {}
for key in paginated.iterkeys():
for key in paginated.keys():
object_list = paginated[key]
if self.settings.get('DEFAULT_PAGINATION'):
@ -148,81 +147,23 @@ class Writer(object):
paginators[key] = Paginator(object_list, len(object_list))
# generated pages, and write
for page_num in range(paginators.values()[0].num_pages):
name_root, ext = os.path.splitext(name)
for page_num in range(list(paginators.values())[0].num_pages):
paginated_localcontext = localcontext.copy()
paginated_name = name
for key in paginators.iterkeys():
for key in paginators.keys():
paginator = paginators[key]
page = paginator.page(page_num + 1)
paginated_localcontext.update(
{'%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)
else:
# no pagination
_write_file(template, localcontext, self.output_path, name)
def update_context_contents(self, name, context):
"""Recursively run the context to find elements (articles, pages, etc)
whose content getter needs to be modified in order to deal with
relative paths.
:param name: name of the file to output.
:param context: dict that will be passed to the templates, which need
to be updated.
"""
def _update_content(name, input):
"""Change all the relatives paths of the input content to relatives
paths suitable fot the ouput content
:param name: path of the output.
:param input: input resource that will be passed to the templates.
"""
content = input._content
hrefs = re.compile(r"""
(?P<markup><\s*[^\>]* # match tag with src and href attr
(?:href|src)\s*=\s*
)
(?P<quote>["\']) # require value to be quoted
(?![#?]) # don't match fragment or query URLs
(?![a-z]+:) # don't match protocol URLS
(?P<path>.*?) # the url value
\2""", re.X)
def replacer(m):
relative_path = m.group('path')
dest_path = os.path.normpath(
os.sep.join((get_relative_path(name), "static",
relative_path)))
return m.group('markup') + m.group('quote') + dest_path \
+ m.group('quote')
return hrefs.sub(replacer, content)
if context is None:
return
if hasattr(context, 'values'):
context = context.values()
for item in context:
# run recursively on iterables
if hasattr(item, '__iter__'):
self.update_context_contents(name, item)
# if it is a content, patch it
elif hasattr(item, '_content'):
relative_path = get_relative_path(name)
paths = self.reminder.setdefault(item, [])
if relative_path not in paths:
paths.append(relative_path)
setattr(item, "_get_content",
partial(_update_content, name, item))