diff --git a/docs/settings.rst b/docs/settings.rst index 2d40d4ec..69e2adc8 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -24,12 +24,7 @@ Basic settings ================================================ ===================================================== Setting name (default value) What does it do? ================================================ ===================================================== -`ARTICLE_PERMALINK_STRUCTURE` (``''``) Empty by default. Enables some customization of URL - structure (see below for more detail). `AUTHOR` Default author (put your name) -`CLEAN_URLS` (``False``) If set to `True`, the URLs will not be suffixed by - `.html`, so you will have to setup URL rewriting on - your web server. `DATE_FORMATS` (``{}``) If you do manage multiple languages, you can set the date formatting here. See "Date format and locales" section below for details. @@ -79,16 +74,14 @@ Setting name (default value) What does it do? .. [#] Default is the system locale. -Article permalink structure ---------------------------- +URL Settings +------------ -This setting allows you to output your articles sorted by date, provided that -you specify a format as specified below. This format follows the Python -``datetime`` directives: - -* %Y: Year with century as a decimal number. -* %m: Month as a decimal number [01,12]. -* %d: Day of the month as a decimal number [01,31]. +You can customize the URL's and locations where files will be saved. The URL's and +SAVE_AS variables use python's format strings. These variables allow you to place +your articles in a location such as '{slug}/index.html' and link to then as +'{slug}' for clean urls. These settings give you the flexibility to place your +articles and pages anywhere you want. Note: If you specify a datetime directive, it will be substituted using the input files' date metadata attribute. If the date is not specified for a @@ -99,15 +92,42 @@ information. Also, you can use other file metadata attributes as well: -* category: '%(category)s' -* author: '%(author)s' -* tags: '%(tags)s' -* date: '%(date)s' +* slug +* date +* lang +* author +* category Example usage: -* '/%Y/%m/' will render something like '/2011/07/sample-post.html'. -* '/%Y/%(category)s/' will render something like '/2011/life/sample-post.html'. +* ARTICLE_URL = 'posts/{date:%Y}/{date:%b}/{date:%d}/{slug}/' +* ARTICLE_SAVE_AS = 'posts/{date:%Y}/{date:%b}/{date:%d}/{slug}/index.html' + +This would save your articles in something like '/posts/2011/Aug/07/sample-post/index.html', +and the URL to this would be '/posts/2011/Aug/07/sample-post/'. + +================================================ ===================================================== +Setting name (default value) what does it do? +================================================ ===================================================== +`ARTICLE_URL` ('{slug}.html') The URL to refer to an ARTICLE. +`ARTICLE_SAVE_AS` ('{slug}.html') The place where we will save an article. +`ARTICLE_LANG_URL` ('{slug}-{lang}.html') The URL to refer to an ARTICLE which doesn't use the + default language. +`ARTICLE_LANG_SAVE_AS` ('{slug}-{lang}.html' The place where we will save an article which + doesn't use the default language. +`PAGE_URL` ('pages/{slug}.html') The URL we will use to link to a page. +`PAGE_SAVE_AS` ('pages/{slug}.html') The location we will save the page. +`PAGE_LANG_URL` ('pages/{slug}-{lang}.html') The URL we will use to link to a page which doesn't + use the default language. +`PAGE_LANG_SAVE_AS` ('pages/{slug}-{lang}.html') The location we will save the page which doesn't + use the default language. +`AUTHOR_URL` ('author/{name}.html') The URL to use for an author. +`AUTHOR_SAVE_AS` ('author/{name}.html') The location to save an author. +`CATEGORY_URL` ('category/{name}.html') The URL to use for a category. +`CATEGORY_SAVE_AS` ('category/{name}.html') The location to save a category. +`TAG_URL` ('tag/{name}.html') The URL to use for a tag. +`TAG_SAVE_AS` ('tag/{name}.html') The location to save the tag page. +================================================ ===================================================== Timezone -------- diff --git a/pelican/__init__.py b/pelican/__init__.py index a0b5704c..bbd0b679 100644 --- a/pelican/__init__.py +++ b/pelican/__init__.py @@ -1,5 +1,6 @@ import argparse import os, sys +import re import time from pelican.generators import (ArticlesGenerator, PagesGenerator, @@ -26,6 +27,42 @@ class Pelican(object): if self.path.endswith('/'): self.path = self.path[:-1] + if settings.get('CLEAN_URLS', False): + log.warning('Found deprecated `CLEAN_URLS` in settings. Modifing' + ' the following settings for the same behaviour.') + + settings['ARTICLE_URL'] = '{slug}/' + settings['ARTICLE_LANG_URL'] = '{slug}-{lang}/' + settings['PAGE_URL'] = 'pages/{slug}/' + settings['PAGE_LANG_URL'] = 'pages/{slug}-{lang}/' + + for setting in ('ARTICLE_URL', 'ARTICLE_LANG_URL', 'PAGE_URL', + 'PAGE_LANG_URL'): + log.warning("%s = '%s'" % (setting, settings[setting])) + + if settings.get('ARTICLE_PERMALINK_STRUCTURE', False): + log.warning('Found deprecated `ARTICLE_PERMALINK_STRUCTURE` in' + ' settings. Modifing the following settings for' + ' the same behaviour.') + + structure = settings['ARTICLE_PERMALINK_STRUCTURE'] + + # Convert %(variable) into {variable}. + structure = re.sub('%\((\w+)\)s', '{\g<1>}', structure) + + # Convert %x into {date:%x} for strftime + structure = re.sub('(%[A-z])', '{date:\g<1>}', structure) + + # Strip a / prefix + structure = re.sub('^/', '', structure) + + for setting in ('ARTICLE_URL', 'ARTICLE_LANG_URL', 'PAGE_URL', + 'PAGE_LANG_URL', 'ARTICLE_SAVE_AS', + 'ARTICLE_LANG_SAVE_AS', 'PAGE_SAVE_AS', + 'PAGE_LANG_SAVE_AS'): + settings[setting] = os.path.join(structure, settings[setting]) + log.warning("%s = '%s'" % (setting, settings[setting])) + # define the default settings self.settings = settings self.theme = theme or settings['THEME'] diff --git a/pelican/contents.py b/pelican/contents.py index 49f30316..90bc189d 100644 --- a/pelican/contents.py +++ b/pelican/contents.py @@ -24,6 +24,7 @@ class Page(object): if not settings: settings = _DEFAULT_CONFIG + self.settings = settings self._content = content self.translations = [] @@ -37,9 +38,9 @@ class Page(object): # default author to the one in settings if not defined if not hasattr(self, 'author'): if 'AUTHOR' in settings: - self.author = settings['AUTHOR'] + self.author = Author(settings['AUTHOR'], settings) else: - self.author = getenv('USER', 'John Doe') + self.author = Author(getenv('USER', 'John Doe'), settings) warning(u"Author of `{0}' unknow, assuming that his name is `{1}'".format(filename or self.title, self.author)) # manage languages @@ -55,29 +56,6 @@ class Page(object): if not hasattr(self, 'slug') and hasattr(self, 'title'): self.slug = slugify(self.title) - # create save_as from the slug (+lang) - if not hasattr(self, 'save_as') and hasattr(self, 'slug'): - if self.in_default_lang: - if settings.get('CLEAN_URLS', False): - self.save_as = '%s/index.html' % self.slug - else: - self.save_as = '%s.html' % self.slug - - clean_url = '%s/' % self.slug - else: - if settings.get('CLEAN_URLS', False): - self.save_as = '%s-%s/index.html' % (self.slug, self.lang) - else: - self.save_as = '%s-%s.html' % (self.slug, self.lang) - - clean_url = '%s-%s/' % (self.slug, self.lang) - - # change the save_as regarding the settings - if settings.get('CLEAN_URLS', False): - self.url = clean_url - elif hasattr(self, 'save_as'): - self.url = self.save_as - if filename: self.filename = filename @@ -115,6 +93,30 @@ class Page(object): if not hasattr(self, prop): raise NameError(prop) + @property + def url_format(self): + return { + 'slug': getattr(self, 'slug', ''), + 'lang': getattr(self, 'lang', 'en'), + 'date': getattr(self, 'date', datetime.datetime.now()), + 'author': self.author, + 'category': getattr(self, 'category', 'misc'), + } + + @property + def url(self): + if self.in_default_lang: + return self.settings.get('PAGE_URL', u'pages/{slug}.html').format(**self.url_format) + + return self.settings.get('PAGE_LANG_URL', u'pages/{slug}-{lang}.html').format(**self.url_format) + + @property + def save_as(self): + if self.in_default_lang: + return self.settings.get('PAGE_SAVE_AS', u'pages/{slug}.html').format(**self.url_format) + + return self.settings.get('PAGE_LANG_SAVE_AS', u'pages/{slug}-{lang}.html').format(**self.url_format) + @property def content(self): if hasattr(self, "_get_content"): @@ -138,10 +140,74 @@ class Page(object): class Article(Page): mandatory_properties = ('title', 'date', 'category') + @property + def url(self): + if self.in_default_lang: + return self.settings.get('ARTICLE_URL', u'{slug}.html').format(**self.url_format) + + return self.settings.get('ARTICLE_LANG_URL', u'{slug}-{lang}.html').format(**self.url_format) + + @property + def save_as(self): + if self.in_default_lang: + return self.settings.get('ARTICLE_SAVE_AS', u'{slug}.html').format(**self.url_format) + + return self.settings.get('ARTICLE_LANG_SAVE_AS', u'{slug}-{lang}.html').format(**self.url_format) + class Quote(Page): base_properties = ('author', 'date') +class URLWrapper(object): + def __init__(self, name, settings): + self.name = unicode(name) + self.settings = settings + + def __hash__(self): + return hash(self.name) + + def __eq__(self, other): + return self.name == unicode(other) + + def __str__(self): + return str(self.name) + + def __unicode__(self): + return self.name + + @property + def url(self): + return '%s.html' % self.name + +class Category(URLWrapper): + @property + def url(self): + return self.settings.get('CATEGORY_URL', u'category/{name}.html').format(name=self.name) + + @property + def save_as(self): + return self.settings.get('CATEGORY_SAVE_AS', u'category/{name}.html').format(name=self.name) + +class Tag(URLWrapper): + def __init__(self, name, *args, **kwargs): + super(Tag, self).__init__(unicode.strip(name), *args, **kwargs) + + @property + def url(self): + return self.settings.get('TAG_URL', u'tag/{name}.html').format(name=self.name) + + @property + def save_as(self): + return self.settings.get('TAG_SAVE_AS', u'tag/{name}.html').format(name=self.name) + +class Author(URLWrapper): + @property + def url(self): + return self.settings.get('AUTHOR_URL', u'author/{name}.html').format(name=self.name) + + @property + def save_as(self): + return self.settings.get('AUTHOR_SAVE_AS', u'author/{name}.html').format(name=self.name) def is_valid_content(content, f): try: diff --git a/pelican/generators.py b/pelican/generators.py index 6715126a..47ebb941 100644 --- a/pelican/generators.py +++ b/pelican/generators.py @@ -13,7 +13,7 @@ from operator import attrgetter, itemgetter from jinja2 import Environment, FileSystemLoader, PrefixLoader, ChoiceLoader from jinja2.exceptions import TemplateNotFound -from pelican.contents import Article, Page, is_valid_content +from pelican.contents import Article, Page, Category, is_valid_content from pelican.log import * from pelican.readers import read_file from pelican.utils import copy, process_translations, open @@ -179,26 +179,26 @@ class ArticlesGenerator(Generator): for tag, articles in self.tags.items(): articles.sort(key=attrgetter('date'), reverse=True) dates = [article for article in self.dates if article in articles] - write('tag/%s.html' % tag, tag_template, self.context, tag=tag, + write(tag.save_as, tag_template, self.context, tag=tag, articles=articles, dates=dates, paginated={'articles': articles, 'dates': dates}, - page_name='tag/%s' % tag) + page_name=u'tag/%s' % tag) category_template = self.get_template('category') for cat, articles in self.categories: dates = [article for article in self.dates if article in articles] - write('category/%s.html' % cat, category_template, self.context, + write(cat.save_as, category_template, self.context, category=cat, articles=articles, dates=dates, paginated={'articles': articles, 'dates': dates}, - page_name='category/%s' % cat) + page_name=u'category/%s' % cat) author_template = self.get_template('author') for aut, articles in self.authors: dates = [article for article in self.dates if article in articles] - write('author/%s.html' % aut, author_template, self.context, + write(aut.save_as, author_template, self.context, author=aut, articles=articles, dates=dates, paginated={'articles': articles, 'dates': dates}, - page_name='author/%s' % aut) + page_name=u'author/%s' % aut) for article in self.drafts: write('drafts/%s.html' % article.slug, article_template, self.context, @@ -212,7 +212,6 @@ class ArticlesGenerator(Generator): files = self.get_files(self.path, exclude=['pages',]) all_articles = [] for f in files: - try: content, metadata = read_file(f, settings=self.settings) except Exception, e: @@ -228,7 +227,7 @@ class ArticlesGenerator(Generator): category = os.path.basename(os.path.dirname(f)).decode('utf-8') if category != '': - metadata['category'] = unicode(category) + metadata['category'] = Category(category, self.settings) if 'date' not in metadata.keys()\ and self.settings['FALLBACK_ON_FS_DATE']: @@ -239,21 +238,6 @@ class ArticlesGenerator(Generator): if not is_valid_content(article, f): continue - add_to_url = u'' - if 'ARTICLE_PERMALINK_STRUCTURE' in self.settings: - article_permalink_structure = self.settings['ARTICLE_PERMALINK_STRUCTURE'] - article_permalink_structure = article_permalink_structure.lstrip('/').replace('%(', "%%(") - - # try to substitute any python datetime directive - add_to_url = article.date.strftime(article_permalink_structure) - # try to substitute any article metadata in rest file - add_to_url = add_to_url % article.__dict__ - add_to_url = [slugify(i) for i in add_to_url.split('/')] - add_to_url = os.path.join(*add_to_url) - - article.url = urlparse.urljoin(add_to_url, article.url) - article.save_as = urlparse.urljoin(add_to_url, article.save_as) - if article.status == "published": if hasattr(article, 'tags'): for tag in article.tags: @@ -348,7 +332,7 @@ class PagesGenerator(Generator): def generate_output(self, writer): for page in chain(self.translations, self.pages): - writer.write_file('pages/%s' % page.save_as, self.get_template('page'), + writer.write_file(page.save_as, self.get_template('page'), self.context, page=page, relative_urls = self.settings.get('RELATIVE_URLS')) diff --git a/pelican/readers.py b/pelican/readers.py index ee3ecd2c..814f81d2 100644 --- a/pelican/readers.py +++ b/pelican/readers.py @@ -15,25 +15,30 @@ except ImportError: Markdown = False import re +from pelican.contents import Category, Tag, Author, URLWrapper from pelican.utils import get_date, open _METADATA_PROCESSORS = { - 'tags': lambda x: map(unicode.strip, unicode(x).split(',')), - 'date': lambda x: get_date(x), - 'status': unicode.strip, + 'tags': lambda x, y: [Tag(tag, y) for tag in unicode(x).split(',')], + 'date': lambda x, y: get_date(x), + 'status': lambda x,y: unicode.strip(x), + 'category': Category, + 'author': Author, } -def _process_metadata(name, value): - if name.lower() in _METADATA_PROCESSORS: - return _METADATA_PROCESSORS[name.lower()](value) - return value - - class Reader(object): enabled = True extensions = None + def __init__(self, settings): + self.settings = settings + + def process_metadata(self, name, value): + if name.lower() in _METADATA_PROCESSORS: + return _METADATA_PROCESSORS[name.lower()](value, self.settings) + return value + class _FieldBodyTranslator(HTMLTranslator): def astext(self): @@ -51,29 +56,25 @@ def render_node_to_html(document, node): node.walkabout(visitor) return visitor.astext() -def get_metadata(document): - """Return the dict containing document metadata""" - output = {} - for docinfo in document.traverse(docutils.nodes.docinfo): - for element in docinfo.children: - if element.tagname == 'field': # custom fields (e.g. summary) - name_elem, body_elem = element.children - name = name_elem.astext() - value = render_node_to_html(document, body_elem) - else: # standard fields (e.g. address) - name = element.tagname - value = element.astext() - - output[name] = _process_metadata(name, value) - return output - - class RstReader(Reader): enabled = bool(docutils) extension = "rst" def _parse_metadata(self, document): - return get_metadata(document) + """Return the dict containing document metadata""" + output = {} + for docinfo in document.traverse(docutils.nodes.docinfo): + for element in docinfo.children: + if element.tagname == 'field': # custom fields (e.g. summary) + name_elem, body_elem = element.children + name = name_elem.astext() + value = render_node_to_html(document, body_elem) + else: # standard fields (e.g. address) + name = element.tagname + value = element.astext() + + output[name] = self.process_metadata(name, value) + return output def _get_publisher(self, filename): extra_params = {'initial_header_level': '2'} @@ -110,7 +111,7 @@ class MarkdownReader(Reader): metadata = {} for name, value in md.Meta.items(): name = name.lower() - metadata[name] = _process_metadata(name, value[0]) + metadata[name] = self.process_metadata(name, value[0]) return content, metadata @@ -126,7 +127,7 @@ class HtmlReader(Reader): key = i.split(':')[0][5:].strip() value = i.split(':')[-1][:-3].strip() name = key.lower() - metadata[name] = _process_metadata(name, value) + metadata[name] = self.process_metadata(name, value) return content, metadata @@ -140,7 +141,7 @@ def read_file(filename, fmt=None, settings=None): fmt = filename.split('.')[-1] if fmt not in _EXTENSIONS.keys(): raise TypeError('Pelican does not know how to parse %s' % filename) - reader = _EXTENSIONS[fmt]() + reader = _EXTENSIONS[fmt](settings) settings_key = '%s_EXTENSIONS' % fmt.upper() if settings and settings_key in settings: reader.extensions = settings[settings_key] diff --git a/pelican/settings.py b/pelican/settings.py index cf6b23e5..ec6ec483 100644 --- a/pelican/settings.py +++ b/pelican/settings.py @@ -26,7 +26,14 @@ _DEFAULT_CONFIG = {'PATH': None, 'REVERSE_ARCHIVE_ORDER': False, 'REVERSE_CATEGORY_ORDER': False, 'DELETE_OUTPUT_DIRECTORY': False, - 'CLEAN_URLS': False, # use /blah/ instead /blah.html in urls + 'ARTICLE_URL': '{slug}.html', + 'ARTICLE_SAVE_AS': '{slug}.html', + 'ARTICLE_LANG_URL': '{slug}-{lang}.html', + 'ARTICLE_LANG_SAVE_AS': '{slug}-{lang}.html', + 'PAGE_URL': 'pages/{slug}.html', + 'PAGE_SAVE_AS': 'pages/{slug}.html', + 'PAGE_LANG_URL': 'pages/{slug}-{lang}.html', + 'PAGE_LANG_SAVE_AS': 'pages/{slug}-{lang}.html', 'RELATIVE_URLS': True, 'DEFAULT_LANG': 'en', 'TAG_CLOUD_STEPS': 4, diff --git a/pelican/themes/notmyidea/templates/article_infos.html b/pelican/themes/notmyidea/templates/article_infos.html index e1803be8..a1993a09 100644 --- a/pelican/themes/notmyidea/templates/article_infos.html +++ b/pelican/themes/notmyidea/templates/article_infos.html @@ -5,10 +5,10 @@ {% if article.author %}
- By {{ article.author }} + By {{ article.author }} {% endif %} -In {{ article.category }}. {% if PDF_PROCESSOR %}get the pdf{% endif %}
+In {{ article.category }}. {% if PDF_PROCESSOR %}get the pdf{% endif %}
{% include 'taglist.html' %} {% include 'translations.html' %} diff --git a/pelican/themes/notmyidea/templates/base.html b/pelican/themes/notmyidea/templates/base.html index 4a4c70c6..12086dc7 100644 --- a/pelican/themes/notmyidea/templates/base.html +++ b/pelican/themes/notmyidea/templates/base.html @@ -31,7 +31,7 @@ {% endfor %} {% if DISPLAY_PAGES_ON_MENU %} {% for page in PAGES %} -tags: {% for tag in article.tags %}{{ tag }}{% endfor %}
{% endif %} +{% if article.tags %}tags: {% for tag in article.tags %}{{ tag }}{% endfor %}
{% endif %} {% if PDF_PROCESSOR %}{% endif %} diff --git a/pelican/themes/simple/templates/article.html b/pelican/themes/simple/templates/article.html index 30d31fb6..d6c96a13 100644 --- a/pelican/themes/simple/templates/article.html +++ b/pelican/themes/simple/templates/article.html @@ -8,7 +8,7 @@ {% if article.author %} - By {{ article.author }} + By {{ article.author }} {% endif %} diff --git a/pelican/themes/simple/templates/base.html b/pelican/themes/simple/templates/base.html index a8fad2db..a1017219 100644 --- a/pelican/themes/simple/templates/base.html +++ b/pelican/themes/simple/templates/base.html @@ -17,7 +17,7 @@ {% endfor %} {% if DISPLAY_PAGES_ON_MENU %} {% for p in PAGES %} -