git merge upstream/master
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals, print_function
|
||||
"""
|
||||
Copyright (c) Marco Milanesi <kpanic@gnufunk.org>
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
||||
|
|
|
|||
78
pelican/plugins/gzip_cache.py
Normal 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)
|
||||
|
||||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
59
pelican/plugins/multi_part.py
Normal 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)
|
||||
52
pelican/plugins/related_posts.py
Normal 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
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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()
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
.hll {
|
||||
background-color:#FFFFCC;
|
||||
background-color:#eee;
|
||||
}
|
||||
.c {
|
||||
color:#408090;
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 963 B After Width: | Height: | Size: 958 B |
|
Before Width: | Height: | Size: 300 B After Width: | Height: | Size: 202 B |
BIN
pelican/themes/notmyidea/static/images/icons/github.png
Normal file
|
After Width: | Height: | Size: 346 B |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 227 B |
BIN
pelican/themes/notmyidea/static/images/icons/gittip.png
Normal file
|
After Width: | Height: | Size: 487 B |
BIN
pelican/themes/notmyidea/static/images/icons/google-plus.png
Normal file
|
After Width: | Height: | Size: 527 B |
|
Before Width: | Height: | Size: 980 B After Width: | Height: | Size: 975 B |
|
Before Width: | Height: | Size: 376 B After Width: | Height: | Size: 896 B |
|
Before Width: | Height: | Size: 896 B After Width: | Height: | Size: 879 B |
|
Before Width: | Height: | Size: 835 B After Width: | Height: | Size: 830 B |
|
|
@ -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 %}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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() }}">
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
9
pelican/themes/simple/templates/translations.html
Normal 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 %}
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
84
pelican/tools/templates/develop_server.sh.in
Executable 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
|
||||
|
|
@ -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/'),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*- #
|
||||
|
||||
import sys
|
||||
sys.path.append('.')
|
||||
from pelicanconf import *
|
||||
|
||||
SITEURL = '$siteurl'
|
||||
|
|
|
|||
277
pelican/utils.py
|
|
@ -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"
|
||||
# --> "Øl trinken beim Besä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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||