mirror of
https://github.com/getpelican/pelican.git
synced 2025-10-15 20:28:56 +02:00
Some doc + various enhancements
This commit is contained in:
parent
5aeca4826c
commit
14cf5f014c
5 changed files with 146 additions and 118 deletions
|
|
@ -13,34 +13,36 @@ import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
from pelican.settings import _DEFAULT_CONFIG
|
|
||||||
from pelican.utils import (slugify, truncate_html_words, memoized,
|
|
||||||
python_2_unicode_compatible, deprecated_attribute)
|
|
||||||
from pelican import signals
|
from pelican import signals
|
||||||
import pelican.utils
|
from pelican.settings import _DEFAULT_CONFIG
|
||||||
|
from pelican.utils import (slugify, truncate_html_words, memoized, strftime,
|
||||||
|
python_2_unicode_compatible, deprecated_attribute)
|
||||||
|
|
||||||
|
# Import these so that they're avalaible when you import from pelican.contents.
|
||||||
|
from pelican.urlwrappers import (URLWrapper, Author, Category, Tag) # NOQA
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Page(object):
|
class Content(object):
|
||||||
"""Represents a page
|
"""Represents a content.
|
||||||
Given a content, and metadata, create an adequate object.
|
|
||||||
|
|
||||||
:param content: the string to parse, containing the original content.
|
:param content: the string to parse, containing the original content.
|
||||||
"""
|
:param metadata: the metadata associated to this page (optional).
|
||||||
mandatory_properties = ('title',)
|
:param settings: the settings dictionary (optional).
|
||||||
default_template = 'page'
|
:param source_path: The location of the source of this content (if any).
|
||||||
|
:param context: The shared context between generators.
|
||||||
|
|
||||||
|
"""
|
||||||
@deprecated_attribute(old='filename', new='source_path', since=(3, 2, 0))
|
@deprecated_attribute(old='filename', new='source_path', since=(3, 2, 0))
|
||||||
def filename():
|
def filename():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __init__(self, content, metadata=None, settings=None,
|
def __init__(self, content, metadata=None, settings=None,
|
||||||
source_path=None, context=None):
|
source_path=None, context=None):
|
||||||
# init parameters
|
if metadata is None:
|
||||||
if not metadata:
|
|
||||||
metadata = {}
|
metadata = {}
|
||||||
if not settings:
|
if settings is None:
|
||||||
settings = copy.deepcopy(_DEFAULT_CONFIG)
|
settings = copy.deepcopy(_DEFAULT_CONFIG)
|
||||||
|
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
|
|
@ -68,6 +70,8 @@ class Page(object):
|
||||||
if 'AUTHOR' in settings:
|
if 'AUTHOR' in settings:
|
||||||
self.author = Author(settings['AUTHOR'], settings)
|
self.author = Author(settings['AUTHOR'], settings)
|
||||||
|
|
||||||
|
# XXX Split all the following code into pieces, there is too much here.
|
||||||
|
|
||||||
# manage languages
|
# manage languages
|
||||||
self.in_default_lang = True
|
self.in_default_lang = True
|
||||||
if 'DEFAULT_LANG' in settings:
|
if 'DEFAULT_LANG' in settings:
|
||||||
|
|
@ -100,8 +104,7 @@ class Page(object):
|
||||||
self.date_format = self.date_format[1]
|
self.date_format = self.date_format[1]
|
||||||
|
|
||||||
if hasattr(self, 'date'):
|
if hasattr(self, 'date'):
|
||||||
self.locale_date = pelican.utils.strftime(self.date,
|
self.locale_date = strftime(self.date, self.date_format)
|
||||||
self.date_format)
|
|
||||||
|
|
||||||
# manage status
|
# manage status
|
||||||
if not hasattr(self, 'status'):
|
if not hasattr(self, 'status'):
|
||||||
|
|
@ -117,13 +120,14 @@ class Page(object):
|
||||||
signals.content_object_init.send(self)
|
signals.content_object_init.send(self)
|
||||||
|
|
||||||
def check_properties(self):
|
def check_properties(self):
|
||||||
"""test that each mandatory property is set."""
|
"""Test mandatory properties are set."""
|
||||||
for prop in self.mandatory_properties:
|
for prop in self.mandatory_properties:
|
||||||
if not hasattr(self, prop):
|
if not hasattr(self, prop):
|
||||||
raise NameError(prop)
|
raise NameError(prop)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url_format(self):
|
def url_format(self):
|
||||||
|
"""Returns the URL, formatted with the proper values"""
|
||||||
metadata = copy.copy(self.metadata)
|
metadata = copy.copy(self.metadata)
|
||||||
metadata.update({
|
metadata.update({
|
||||||
'slug': getattr(self, 'slug', ''),
|
'slug': getattr(self, 'slug', ''),
|
||||||
|
|
@ -146,12 +150,14 @@ class Page(object):
|
||||||
return self._expand_settings(key)
|
return self._expand_settings(key)
|
||||||
|
|
||||||
def _update_content(self, content, siteurl):
|
def _update_content(self, content, siteurl):
|
||||||
"""Change all the relative paths of the content to relative paths
|
"""Update the content attribute.
|
||||||
|
|
||||||
|
Change all the relative paths of the content to relative paths
|
||||||
suitable for the ouput content.
|
suitable for the ouput content.
|
||||||
|
|
||||||
:param content: content resource that will be passed to the templates.
|
:param content: content resource that will be passed to the templates.
|
||||||
:param siteurl: siteurl which is locally generated by the writer in
|
:param siteurl: siteurl which is locally generated by the writer in
|
||||||
case of RELATIVE_URLS.
|
case of RELATIVE_URLS.
|
||||||
"""
|
"""
|
||||||
hrefs = re.compile(r"""
|
hrefs = re.compile(r"""
|
||||||
(?P<markup><\s*[^\>]* # match tag with src and href attr
|
(?P<markup><\s*[^\>]* # match tag with src and href attr
|
||||||
|
|
@ -165,9 +171,12 @@ class Page(object):
|
||||||
what = m.group('what')
|
what = m.group('what')
|
||||||
value = m.group('value')
|
value = m.group('value')
|
||||||
origin = m.group('path')
|
origin = m.group('path')
|
||||||
|
|
||||||
# we support only filename for now. the plan is to support
|
# we support only filename for now. the plan is to support
|
||||||
# categories, tags, etc. in the future, but let's keep things
|
# categories, tags, etc. in the future, but let's keep things
|
||||||
# simple for now.
|
# simple for now.
|
||||||
|
|
||||||
|
# XXX Put this in a different location.
|
||||||
if what == 'filename':
|
if what == 'filename':
|
||||||
if value.startswith('/'):
|
if value.startswith('/'):
|
||||||
value = value[1:]
|
value = value[1:]
|
||||||
|
|
@ -191,18 +200,23 @@ class Page(object):
|
||||||
|
|
||||||
@memoized
|
@memoized
|
||||||
def get_content(self, siteurl):
|
def get_content(self, siteurl):
|
||||||
return self._update_content(
|
|
||||||
self._get_content() if hasattr(self, "_get_content")
|
if hasattr(self, '_get_content'):
|
||||||
else self._content,
|
content = self._get_content()
|
||||||
siteurl)
|
else:
|
||||||
|
content = self._content
|
||||||
|
return self._update_content(content, siteurl)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def content(self):
|
def content(self):
|
||||||
return self.get_content(self._context['localsiteurl'])
|
return self.get_content(self._context['localsiteurl'])
|
||||||
|
|
||||||
def _get_summary(self):
|
def _get_summary(self):
|
||||||
"""Returns the summary of an article, based on the summary metadata
|
"""Returns the summary of an article.
|
||||||
if it is set, else truncate the content."""
|
|
||||||
|
This is based on the summary metadata if set, otherwise truncate the
|
||||||
|
content.
|
||||||
|
"""
|
||||||
if hasattr(self, '_summary'):
|
if hasattr(self, '_summary'):
|
||||||
return self._summary
|
return self._summary
|
||||||
|
|
||||||
|
|
@ -217,7 +231,6 @@ class Page(object):
|
||||||
|
|
||||||
summary = property(_get_summary, _set_summary, "Summary of the article."
|
summary = property(_get_summary, _set_summary, "Summary of the article."
|
||||||
"Based on the content. Can't be set")
|
"Based on the content. Can't be set")
|
||||||
|
|
||||||
url = property(functools.partial(get_url_setting, key='url'))
|
url = property(functools.partial(get_url_setting, key='url'))
|
||||||
save_as = property(functools.partial(get_url_setting, key='save_as'))
|
save_as = property(functools.partial(get_url_setting, key='save_as'))
|
||||||
|
|
||||||
|
|
@ -250,6 +263,11 @@ class Page(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Page(Content):
|
||||||
|
mandatory_properties = ('title',)
|
||||||
|
default_template = 'page'
|
||||||
|
|
||||||
|
|
||||||
class Article(Page):
|
class Article(Page):
|
||||||
mandatory_properties = ('title', 'date', 'category')
|
mandatory_properties = ('title', 'date', 'category')
|
||||||
default_template = 'article'
|
default_template = 'article'
|
||||||
|
|
@ -259,78 +277,9 @@ class Quote(Page):
|
||||||
base_properties = ('author', 'date')
|
base_properties = ('author', 'date')
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
@functools.total_ordering
|
|
||||||
class URLWrapper(object):
|
|
||||||
def __init__(self, name, settings):
|
|
||||||
self.name = name
|
|
||||||
self.slug = slugify(self.name)
|
|
||||||
self.settings = settings
|
|
||||||
|
|
||||||
def as_dict(self):
|
|
||||||
return self.__dict__
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(self.name)
|
|
||||||
|
|
||||||
def _key(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
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, six.string_types):
|
|
||||||
logger.warning('%s is set to %s' % (setting, value))
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
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'))
|
|
||||||
|
|
||||||
|
|
||||||
class Category(URLWrapper):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(URLWrapper):
|
|
||||||
def __init__(self, name, *args, **kwargs):
|
|
||||||
super(Tag, self).__init__(name.strip(), *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class Author(URLWrapper):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class StaticContent(object):
|
class StaticContent(object):
|
||||||
|
|
||||||
@deprecated_attribute(old='filepath', new='source_path', since=(3, 2, 0))
|
@deprecated_attribute(old='filepath', new='source_path', since=(3, 2, 0))
|
||||||
def filepath():
|
def filepath():
|
||||||
return None
|
return None
|
||||||
|
|
@ -340,6 +289,7 @@ class StaticContent(object):
|
||||||
settings = copy.deepcopy(_DEFAULT_CONFIG)
|
settings = copy.deepcopy(_DEFAULT_CONFIG)
|
||||||
self.src = src
|
self.src = src
|
||||||
self.url = dst or src
|
self.url = dst or src
|
||||||
|
|
||||||
# On Windows, make sure we end up with Unix-like paths.
|
# On Windows, make sure we end up with Unix-like paths.
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
self.url = self.url.replace('\\', '/')
|
self.url = self.url.replace('\\', '/')
|
||||||
|
|
@ -355,6 +305,6 @@ def is_valid_content(content, f):
|
||||||
content.check_properties()
|
content.check_properties()
|
||||||
return True
|
return True
|
||||||
except NameError as e:
|
except NameError as e:
|
||||||
logger.warning("Skipping %s: impossible to find informations about "
|
logger.error("Skipping %s: impossible to find informations about "
|
||||||
"'%s'" % (f, e))
|
"'%s'" % (f, e))
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ _DEFAULT_CONFIG = {'PATH': '.',
|
||||||
'DEFAULT_DATE_FORMAT': '%a %d %B %Y',
|
'DEFAULT_DATE_FORMAT': '%a %d %B %Y',
|
||||||
'DATE_FORMATS': {},
|
'DATE_FORMATS': {},
|
||||||
'JINJA_EXTENSIONS': [],
|
'JINJA_EXTENSIONS': [],
|
||||||
'LOCALE': '', # default to user locale
|
'LOCALE': '', # defaults to user locale
|
||||||
'DEFAULT_PAGINATION': False,
|
'DEFAULT_PAGINATION': False,
|
||||||
'DEFAULT_ORPHANS': 0,
|
'DEFAULT_ORPHANS': 0,
|
||||||
'DEFAULT_METADATA': (),
|
'DEFAULT_METADATA': (),
|
||||||
|
|
@ -107,9 +107,7 @@ def read_settings(path=None, override=None):
|
||||||
|
|
||||||
|
|
||||||
def get_settings_from_module(module=None, default_settings=_DEFAULT_CONFIG):
|
def get_settings_from_module(module=None, default_settings=_DEFAULT_CONFIG):
|
||||||
"""
|
"""Loads settings from a module, returns a dictionary."""
|
||||||
Load settings from a module, returning a dict.
|
|
||||||
"""
|
|
||||||
|
|
||||||
context = copy.deepcopy(default_settings)
|
context = copy.deepcopy(default_settings)
|
||||||
if module is not None:
|
if module is not None:
|
||||||
|
|
@ -119,10 +117,7 @@ def get_settings_from_module(module=None, default_settings=_DEFAULT_CONFIG):
|
||||||
|
|
||||||
|
|
||||||
def get_settings_from_file(path, default_settings=_DEFAULT_CONFIG):
|
def get_settings_from_file(path, default_settings=_DEFAULT_CONFIG):
|
||||||
"""
|
"""Loads settings from a file path, returning a dict."""
|
||||||
Load settings from a file path, returning a dict.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
name = os.path.basename(path).rpartition('.')[0]
|
name = os.path.basename(path).rpartition('.')[0]
|
||||||
module = imp.load_source(name, path)
|
module = imp.load_source(name, path)
|
||||||
|
|
@ -130,14 +125,15 @@ def get_settings_from_file(path, default_settings=_DEFAULT_CONFIG):
|
||||||
|
|
||||||
|
|
||||||
def configure_settings(settings):
|
def configure_settings(settings):
|
||||||
"""
|
"""Provide optimizations, error checking and warnings for the given
|
||||||
Provide optimizations, error checking, and warnings for loaded settings
|
settings.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not 'PATH' in settings or not os.path.isdir(settings['PATH']):
|
if not 'PATH' in settings or not os.path.isdir(settings['PATH']):
|
||||||
raise Exception('You need to specify a path containing the content'
|
raise Exception('You need to specify a path containing the content'
|
||||||
' (see pelican --help for more information)')
|
' (see pelican --help for more information)')
|
||||||
|
|
||||||
# find the theme in pelican.theme if the given one does not exists
|
# lookup the theme in "pelican/themes" if the given one doesn't exist
|
||||||
if not os.path.isdir(settings['THEME']):
|
if not os.path.isdir(settings['THEME']):
|
||||||
theme_path = os.sep.join([os.path.dirname(
|
theme_path = os.sep.join([os.path.dirname(
|
||||||
os.path.abspath(__file__)), "themes/%s" % settings['THEME']])
|
os.path.abspath(__file__)), "themes/%s" % settings['THEME']])
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ class TestPelican(LoggedTestCase):
|
||||||
super(TestPelican, self).setUp()
|
super(TestPelican, self).setUp()
|
||||||
self.temp_path = mkdtemp()
|
self.temp_path = mkdtemp()
|
||||||
self.old_locale = locale.setlocale(locale.LC_ALL)
|
self.old_locale = locale.setlocale(locale.LC_ALL)
|
||||||
|
self.maxDiff = None
|
||||||
locale.setlocale(locale.LC_ALL, str('C'))
|
locale.setlocale(locale.LC_ALL, str('C'))
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
|
|
||||||
79
pelican/urlwrappers.py
Normal file
79
pelican/urlwrappers.py
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import os
|
||||||
|
import functools
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from pelican.utils import (slugify, python_2_unicode_compatible)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
@functools.total_ordering
|
||||||
|
class URLWrapper(object):
|
||||||
|
def __init__(self, name, settings):
|
||||||
|
self.name = name
|
||||||
|
self.slug = slugify(self.name)
|
||||||
|
self.settings = settings
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
return self.__dict__
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.name)
|
||||||
|
|
||||||
|
def _key(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
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, six.string_types):
|
||||||
|
logger.warning('%s is set to %s' % (setting, value))
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
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'))
|
||||||
|
|
||||||
|
|
||||||
|
class Category(URLWrapper):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Tag(URLWrapper):
|
||||||
|
def __init__(self, name, *args, **kwargs):
|
||||||
|
super(Tag, self).__init__(name.strip(), *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Author(URLWrapper):
|
||||||
|
pass
|
||||||
|
|
@ -312,13 +312,13 @@ def get_relative_path(path):
|
||||||
|
|
||||||
|
|
||||||
def truncate_html_words(s, num, end_text='...'):
|
def truncate_html_words(s, num, end_text='...'):
|
||||||
"""Truncates HTML to a certain number of words (not counting tags and
|
"""Truncates HTML to a certain number of words.
|
||||||
comments). Closes opened tags if they were correctly closed in the given
|
|
||||||
html. Takes an optional argument of what should be used to notify that the
|
|
||||||
string has been truncated, defaulting to ellipsis (...).
|
|
||||||
|
|
||||||
Newlines in the HTML are preserved.
|
(not counting tags and comments). Closes opened tags if they were correctly
|
||||||
From the django framework.
|
closed in the given html. Takes an optional argument of what should be used
|
||||||
|
to notify that the string has been truncated, defaulting to ellipsis (...).
|
||||||
|
|
||||||
|
Newlines in the HTML are preserved. (From the django framework).
|
||||||
"""
|
"""
|
||||||
length = int(num)
|
length = int(num)
|
||||||
if length <= 0:
|
if length <= 0:
|
||||||
|
|
@ -382,11 +382,13 @@ def truncate_html_words(s, num, end_text='...'):
|
||||||
|
|
||||||
|
|
||||||
def process_translations(content_list):
|
def process_translations(content_list):
|
||||||
""" Finds all translation and returns tuple with two lists (index,
|
""" Finds translation and returns them.
|
||||||
translations). Index list includes items in default language or items
|
|
||||||
which have no variant in default language.
|
|
||||||
|
|
||||||
Also, for each content_list item, it sets attribute 'translations'
|
Returns a tuple with two lists (index, translations). Index list includes
|
||||||
|
items in default language or items which have no variant in default
|
||||||
|
language.
|
||||||
|
|
||||||
|
For each content_list item, sets the 'translations' attribute.
|
||||||
"""
|
"""
|
||||||
content_list.sort(key=attrgetter('slug'))
|
content_list.sort(key=attrgetter('slug'))
|
||||||
grouped_by_slugs = groupby(content_list, attrgetter('slug'))
|
grouped_by_slugs = groupby(content_list, attrgetter('slug'))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue