This commit is contained in:
Jiachen Yang 2012-03-21 11:02:58 +09:00
commit 5435c55f3e
22 changed files with 164 additions and 102 deletions

View file

@ -6,3 +6,8 @@ install:
- pip install nose unittest2 mock --use-mirrors
- pip install . --use-mirrors
script: nosetests -s tests
notifications:
irc:
channels:
- "irc.freenode.org#pelican"
on_success: change

View file

@ -2,4 +2,3 @@ include *.rst
global-include *.py
recursive-include pelican *.html *.css *png
include LICENSE
global-include *.bat

View file

@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
import sys, os
sys.path.append(os.path.abspath('..'))
from pelican import __version__, __major__
# -- General configuration -----------------------------------------------------
templates_path = ['_templates']
extensions = ['sphinx.ext.autodoc',]
@ -9,12 +13,11 @@ master_doc = 'index'
project = u'Pelican'
copyright = u'2010, Alexis Metaireau and contributors'
exclude_patterns = ['_build']
version = "2"
release = version
version = __version__
release = __major__
# -- Options for HTML output ---------------------------------------------------
sys.path.append(os.path.abspath('_themes'))
html_theme_path = ['_themes']
html_theme = 'pelican'

View file

@ -46,7 +46,7 @@ Setting name (default value) What does it do?
`MARKUP` (``('rst', 'md')``) A list of available markup languages you want
to use. For the moment, the only available values
are `rst` and `md`.
`MD_EXTENSIONS` (``('codehilite','extra')``) A list of the extensions that the Markdown processor
`MD_EXTENSIONS` (``['codehilite','extra']``) A list of the extensions that the Markdown processor
will use. Refer to the extensions chapter in the
Python-Markdown documentation for a complete list of
supported extensions.
@ -83,7 +83,7 @@ Setting name (default value) What does it do?
.. [#] Default is the system locale.
URL Settings
URL settings
------------
You can customize the URL's and locations where files will be saved. The URL's and
@ -160,6 +160,8 @@ maintain multiple languages with different date formats, you can set this dict
using language name (``lang`` in your posts) as key. Regarding available format
codes, see `strftime document of python`_ :
.. parsed-literal::
DATE_FORMAT = {
'en': '%a, %d %b %Y',
'jp': '%Y-%m-%d(%a)',
@ -167,6 +169,8 @@ codes, see `strftime document of python`_ :
You can set locale to further control date format:
.. parsed-literal::
LOCALE = ('usa', 'jpn', # On Windows
'en_US', 'ja_JP' # On Unix/Linux
)
@ -175,6 +179,7 @@ Also, it is possible to set different locale settings for each language. If you
put (locale, format) tuples in the dict, this will override the LOCALE setting
above:
.. parsed-literal::
# On Unix/Linux
DATE_FORMAT = {
'en': ('en_US','%a, %d %b %Y'),

View file

@ -11,7 +11,9 @@ from pelican.utils import clean_output_dir, files_changed
from pelican.writers import Writer
from pelican import log
__version__ = "3.0"
__major__ = 3
__minor__ = 0
__version__ = "{0}.{1}".format(__major__, __minor__)
class Pelican(object):
@ -135,7 +137,8 @@ class Pelican(object):
def main():
parser = argparse.ArgumentParser(description="""A tool to generate a
static blog, with restructured text input files.""")
static blog, with restructured text input files.""",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(dest='path', nargs='?',
help='Path where to find the content files.')
@ -145,11 +148,11 @@ def main():
parser.add_argument('-o', '--output', dest='output',
help='Where to output the generated files. If not specified, a '
'directory will be created, named "output" in the current path.')
parser.add_argument('-m', '--markup', default=None, dest='markup',
parser.add_argument('-m', '--markup', dest='markup',
help='The list of markup language to use (rst or md). Please indicate '
'them separated by commas.')
parser.add_argument('-s', '--settings', dest='settings', default='',
help='The settings of the application. Default to False.')
parser.add_argument('-s', '--settings', dest='settings',
help='The settings of the application.')
parser.add_argument('-d', '--delete-output-directory',
dest='delete_outputdir',
action='store_true', help='Delete the output directory.')

View file

@ -42,9 +42,10 @@ class Page(object):
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)
warning(u"Author of `{0}' unknown, assuming that his name is "
"`{1}'".format(filename or self.title, self.author))
"`{1}'".format(title, self.author))
# manage languages
self.in_default_lang = True
@ -89,9 +90,9 @@ class Page(object):
if hasattr(self, 'date') and self.date > datetime.now():
self.status = 'draft'
# set summary
if not hasattr(self, 'summary'):
self.summary = truncate_html_words(self.content, 50)
# store the summary metadata if it is set
if 'summary' in metadata:
self._summary = metadata['summary']
def check_properties(self):
"""test that each mandatory property is set."""
@ -126,8 +127,12 @@ class Page(object):
return content
def _get_summary(self):
"""Returns the summary of an article, based on to the content"""
return truncate_html_words(self.content, 50)
"""Returns the summary of an article, based on the summary metadata
if it is set, else troncate the content."""
if hasattr(self, '_summary'):
return self._summary
else:
return truncate_html_words(self.content, 50)
def _set_summary(self, summary):
"""Dummy function"""

View file

@ -227,7 +227,7 @@ class ArticlesGenerator(Generator):
continue
# if no category is set, use the name of the path as a category
if 'category' not in metadata.keys():
if 'category' not in metadata:
if os.path.dirname(f) == self.path:
category = self.settings['DEFAULT_CATEGORY']
@ -238,8 +238,7 @@ class ArticlesGenerator(Generator):
if category != '':
metadata['category'] = Category(category, self.settings)
if 'date' not in metadata.keys()\
and self.settings['FALLBACK_ON_FS_DATE']:
if 'date' not in metadata and self.settings['FALLBACK_ON_FS_DATE']:
metadata['date'] = datetime.datetime.fromtimestamp(
os.stat(f).st_ctime)

View file

@ -1,38 +1,32 @@
import os
import sys
from logging import CRITICAL, ERROR, WARN, INFO, DEBUG
from logging import CRITICAL, ERROR, WARN, INFO, DEBUG
from logging import critical, error, info, warning, warn, debug
from logging import Formatter, getLogger, StreamHandler
RESET_TERM = u'\033[1;m'
RESET_TERM = u'\033[0;m'
def term_color(code):
return lambda text: code + unicode(text) + RESET_TERM
def start_color(index):
return u'\033[1;{0}m'.format(index)
def term_color(color):
code = COLOR_CODES[color]
return lambda text: start_color(code) + unicode(text) + RESET_TERM
COLOR_CODES = {
'gray': u'\033[1;30m',
'red': u'\033[1;31m',
'green': u'\033[1;32m',
'yellow': u'\033[1;33m',
'blue': u'\033[1;34m',
'magenta': u'\033[1;35m',
'cyan': u'\033[1;36m',
'white': u'\033[1;37m',
'bgred': u'\033[1;41m',
'bggreen': u'\033[1;42m',
'bgbrown': u'\033[1;43m',
'bgblue': u'\033[1;44m',
'bgmagenta': u'\033[1;45m',
'bgcyan': u'\033[1;46m',
'bggray': u'\033[1;47m',
'bgyellow': u'\033[1;43m',
'bggrey': u'\033[1;100m',
'red': 31,
'yellow': 33,
'cyan': 36,
'white': 37,
'bgred': 41,
'bggrey': 100,
}
ANSI = dict((col, term_color(code)) for col, code in COLOR_CODES.items())
ANSI = dict((col, term_color(col)) for col in COLOR_CODES)
class ANSIFormatter(Formatter):
@ -80,7 +74,7 @@ class DummyFormatter(object):
and not sys.platform.startswith('win'):
return ANSIFormatter(*args, **kwargs)
else:
return TextFormatter( *args, **kwargs)
return TextFormatter(*args, **kwargs)
def init(level=None, logger=getLogger(), handler=StreamHandler()):

View file

@ -36,8 +36,8 @@ class Reader(object):
self.settings = settings
def process_metadata(self, name, value):
if name.lower() in _METADATA_PROCESSORS:
return _METADATA_PROCESSORS[name.lower()](value, self.settings)
if name in _METADATA_PROCESSORS:
return _METADATA_PROCESSORS[name](value, self.settings)
return value
@ -71,10 +71,14 @@ class RstReader(Reader):
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)
if name == 'summary':
value = render_node_to_html(document, body_elem)
else:
value = body_elem.astext()
else: # standard fields (e.g. address)
name = element.tagname
value = element.astext()
name = name.lower()
output[name] = self.process_metadata(name, value)
return output
@ -144,7 +148,7 @@ def read_file(filename, fmt=None, settings=None):
if not fmt:
fmt = filename.split('.')[-1]
if fmt not in _EXTENSIONS.keys():
if fmt not in _EXTENSIONS:
raise TypeError('Pelican does not know how to parse %s' % filename)
reader = _EXTENSIONS[fmt](settings)

View file

@ -6,7 +6,7 @@
<dl>
{% for article in dates %}
<dt>{{ article.locale_date }}</dt>
<dd><a href='{{ article.url }}'>{{ article.title }}</a></dd>
<dd><a href="{{ article.url }}">{{ article.title }}</a></dd>
{% endfor %}
</dl>
</section>

View file

@ -1,11 +1,11 @@
{% extends "base.html" %}
{% block content %}
<h1>Archives for {{ SITENAME }}</h2>
<h1>Archives for {{ SITENAME }}</h1>
<dl>
{% for article in dates %}
<dt>{{ article.locale_date }}</dt>
<dd><a href='{{ article.url }}'>{{ article.title }}</a></dd>
<dd><a href="{{ article.url }}">{{ article.title }}</a></dd>
{% endfor %}
</dl>
{% endblock %}

View file

@ -2,6 +2,8 @@
import argparse
import os
import subprocess
import sys
import time
from codecs import open
@ -38,7 +40,7 @@ def wp2fields(xml):
def dc2fields(file):
"""Opens a Dotclear export file, and yield pelican fields"""
from BeautifulSoup import BeautifulStoneSoup, BeautifulSoup
from BeautifulSoup import BeautifulStoneSoup
in_cat = False
in_post = False
@ -85,10 +87,10 @@ def dc2fields(file):
post_creadt = fields[6]
# post_upddt = fields[7]
# post_password = fields[8]
post_type = fields[9]
# post_type = fields[9]
post_format = fields[10]
post_url = fields[11]
post_lang = fields[12]
# post_url = fields[11]
# post_lang = fields[12]
post_title = fields[13]
post_excerpt = fields[14]
post_excerpt_xhtml = fields[15]
@ -216,7 +218,20 @@ def fields2pelican(fields, out_markup, output_path, dircat=False):
content = content.replace("\n", "<br />\n")
fp.write(content)
os.system('pandoc --normalize --reference-links --from=html --to=%s -o "%s" "%s"' % (out_markup, out_filename, html_filename))
cmd = 'pandoc --normalize --reference-links --from=html --to={0} -o "{1}" "{2}"'.format(
out_markup, out_filename, html_filename)
try:
rc = subprocess.call(cmd, shell=True)
if rc < 0:
print("Child was terminated by signal %d" % -rc)
exit()
elif rc > 0:
print("Please, check your Pandoc installation.")
exit()
except OSError, e:
print("Pandoc execution failed: %s" % e)
exit()
os.remove(html_filename)
@ -234,7 +249,8 @@ def fields2pelican(fields, out_markup, output_path, dircat=False):
def main():
parser = argparse.ArgumentParser(
description="Transform feed, Wordpress or Dotclear files to rst files."
"Be sure to have pandoc installed")
"Be sure to have pandoc installed",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(dest='input', help='The input file to read')
parser.add_argument('--wpfile', action='store_true', dest='wpfile',
@ -259,24 +275,21 @@ def main():
elif args.feed:
input_type = 'feed'
else:
print("you must provide either --wpfile, --dotclear or --feed options")
print("You must provide either --wpfile, --dotclear or --feed options")
exit()
if not os.path.exists(args.output):
try:
os.mkdir(args.output)
except OSError:
error("Couldn't create the output folder: " + args.output)
print("Unable to create the output folder: " + args.output)
exit()
# TODO: refactor this long assignment
input_type, input, out_markup, output_path, dircat=False = input_type, args.input, args.markup, args.output, args.dircat
if input_type == 'wordpress':
fields = wp2fields(input)
fields = wp2fields(args.input)
elif input_type == 'dotclear':
fields = dc2fields(input)
fields = dc2fields(args.input)
elif input_type == 'feed':
fields = feed2fields(input)
fields = feed2fields(args.input)
fields2pelican(fields, out_markup, output_path, dircat=dircat)
fields2pelican(fields, args.markup, args.output, dircat=args.dircat or False)

View file

@ -1,10 +1,13 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*- #
import os, sys, argparse, string
import os
import string
import argparse
from pelican import __version__
TEMPLATES={
TEMPLATES = {
'Makefile' : '''
PELICAN=$pelican
PELICANOPTS=$pelicanopts
@ -60,7 +63,7 @@ github: $$(OUTPUTDIR)/index.html
\tgit push origin gh-pages
.PHONY: html help clean ftp_upload ssh_upload dropbox_upload github
''',
''',
'pelican.conf.py': '''#!/usr/bin/env python
# -*- coding: utf-8 -*- #
@ -87,9 +90,7 @@ SOCIAL = (
)
DEFAULT_PAGINATION = $default_pagination
'''
'''
}
CONF = {
@ -108,17 +109,6 @@ CONF = {
}
class _dict(dict):
def __init__(self, *args, **kwargs):
dict.__init__(self, *args, **kwargs)
def __getitem__(self, i):
return dict.get(self,i,None)
def has_key(k):
return True
def ask(question, answer=str, default=None, l=None):
if answer == str:
r = ''
@ -193,14 +183,16 @@ def ask(question, answer=str, default=None, l=None):
def main():
parser = argparse.ArgumentParser(description="A kickstarter for pelican")
parser = argparse.ArgumentParser(
description="A kickstarter for pelican",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('-p', '--path', default=".",
help="The path to generate the blog into")
parser.add_argument('-t', '--title', default=None, metavar="title",
parser.add_argument('-t', '--title', metavar="title",
help='Set the title of the website')
parser.add_argument('-a', '--author', default=None, metavar="author",
parser.add_argument('-a', '--author', metavar="author",
help='Set the author name of the website')
parser.add_argument('-l', '--lang', default=None, metavar="lang",
parser.add_argument('-l', '--lang', metavar="lang",
help='Set the default lang of the website')
args = parser.parse_args()

View file

@ -210,9 +210,6 @@ LAST_MTIME = 0
def files_changed(path, extensions):
"""Return True if the files have changed since the last check"""
def with_extension(f):
return any(f.endswith(ext) for ext in extensions)
def file_times(path):
"""Return the last time files have been modified"""
for root, dirs, files in os.walk(path):

View file

@ -1,8 +1,6 @@
#!/usr/bin/env python
from setuptools import setup
VERSION = "3.0" # find a better way to do so.
requires = ['feedgenerator', 'jinja2', 'pygments', 'docutils', 'pytz']
try:
@ -13,21 +11,21 @@ except ImportError:
entry_points = {
'console_scripts': [
'pelican = pelican:main',
'pelican-import = tools.pelican_import:main',
'pelican-quickstart = tools.pelican_quickstart:main',
'pelican-themes = tools.pelican_themes:main'
]
'pelican-import = pelican.tools.pelican_import:main',
'pelican-quickstart = pelican.tools.pelican_quickstart:main',
'pelican-themes = pelican.tools.pelican_themes:main'
]
}
setup(
name = "pelican",
version = VERSION,
version = "3.0",
url = 'http://pelican.notmyidea.org/',
author = 'Alexis Metaireau',
author_email = 'alexis@notmyidea.org',
description = "A tool to generate a static blog from reStructuredText or Markdown input files.",
long_description=open('README.rst').read(),
packages = ['pelican'],
packages = ['pelican', 'pelican.tools'],
include_package_data = True,
install_requires = requires,
entry_points = entry_points,

View file

@ -9,3 +9,4 @@ This is a super article !
:summary:
Multi-line metadata should be supported
as well as **inline markup**.
:custom_field: http://notmyidea.org

View file

@ -0,0 +1,6 @@
This is a super article !
#########################
:Category: Yeah

View file

@ -8,14 +8,20 @@ except ImportError, e:
from pelican.contents import Page
from pelican.settings import _DEFAULT_CONFIG
from jinja2.utils import generate_lorem_ipsum
# generate one paragraph, enclosed with <p>
TEST_CONTENT = str(generate_lorem_ipsum(n=1))
TEST_SUMMARY = generate_lorem_ipsum(n=1, html=False)
class TestPage(TestCase):
def setUp(self):
super(TestPage, self).setUp()
self.page_kwargs = {
'content': 'content',
'content': TEST_CONTENT,
'metadata': {
'summary': TEST_SUMMARY,
'title': 'foo bar',
'author': 'Blogger',
},
@ -27,11 +33,11 @@ class TestPage(TestCase):
"""
metadata = {'foo': 'bar', 'foobar': 'baz', 'title': 'foobar', }
page = Page('content', metadata=metadata)
page = Page(TEST_CONTENT, metadata=metadata)
for key, value in metadata.items():
self.assertTrue(hasattr(page, key))
self.assertEqual(value, getattr(page, key))
self.assertEqual(page.content, 'content')
self.assertEqual(page.content, TEST_CONTENT)
def test_mandatory_properties(self):
"""If the title is not set, must throw an exception."""
@ -39,6 +45,11 @@ class TestPage(TestCase):
page = Page(**self.page_kwargs)
page.check_properties()
def test_summary_from_metadata(self):
"""If a :summary: metadata is given, it should be used."""
page = Page(**self.page_kwargs)
self.assertEqual(page.summary, TEST_SUMMARY)
def test_slug(self):
"""If a title is given, it should be used to generate the slug."""
page = Page(**self.page_kwargs)

View file

@ -30,11 +30,20 @@ class RstReaderTest(unittest.TestCase):
' <strong>inline markup</strong>.',
'date': datetime.datetime(2010, 12, 2, 10, 14),
'tags': ['foo', 'bar', 'foobar'],
'custom_field': 'http://notmyidea.org',
}
for key, value in expected.items():
self.assertEquals(value, metadata[key], key)
def test_article_metadata_key_lowercase(self):
"""Keys of metadata should be lowercase."""
reader = readers.RstReader({})
content, metadata = reader.read(_filename('article_with_uppercase_metadata.rst'))
self.assertIn('category', metadata, "Key should be lowercase.")
self.assertEquals('Yeah', metadata.get('category'), "Value keeps cases.")
def test_typogrify(self):
# if nothing is specified in the settings, the content should be
# unmodified

View file

@ -3,11 +3,12 @@ try:
import unittest2 as unittest
except ImportError:
import unittest # NOQA
import os
import datetime
import time
from pelican import utils
from pelican.contents import Article
from support import get_article
@ -73,3 +74,20 @@ class TestUtils(unittest.TestCase):
self.assertIn(fr_article1, trans)
self.assertNotIn(en_article1, trans)
self.assertNotIn(fr_article1, index)
def test_files_changed(self):
"Test if file changes are correctly detected"
path = os.path.join(os.path.dirname(__file__), 'content')
filename = os.path.join(path, 'article_with_metadata.rst')
changed = utils.files_changed(path, 'rst')
self.assertEquals(changed, True)
changed = utils.files_changed(path, 'rst')
self.assertEquals(changed, False)
t = time.time()
os.utime(filename, (t, t))
changed = utils.files_changed(path, 'rst')
self.assertEquals(changed, True)
self.assertAlmostEqual(utils.LAST_MTIME, t, places=2)