aboutsummaryrefslogtreecommitdiffstats
path: root/nikola/plugins
diff options
context:
space:
mode:
authorLibravatarAgustin Henze <tin@sluc.org.ar>2013-05-30 17:41:06 -0300
committerLibravatarAgustin Henze <tin@sluc.org.ar>2013-05-30 17:41:06 -0300
commit0c4dfdec5b55b6064dccc38bbfb0a7c0699c895a (patch)
treea6707225ccc559f7edf50ddd3fdc7fc85145c921 /nikola/plugins
parent8b14a1e5b2ca574fdd4fd2377567ec98a110d4b6 (diff)
Imported Upstream version 5.4.4
Diffstat (limited to 'nikola/plugins')
-rw-r--r--nikola/plugins/command_check.py21
-rw-r--r--nikola/plugins/command_console.py76
-rw-r--r--nikola/plugins/command_deploy.py26
-rw-r--r--nikola/plugins/command_import_blogger.py17
-rw-r--r--nikola/plugins/command_import_wordpress.py32
-rw-r--r--nikola/plugins/command_install_theme.py4
-rw-r--r--nikola/plugins/command_new_post.py36
-rw-r--r--nikola/plugins/command_planetoid.plugin9
-rw-r--r--nikola/plugins/command_planetoid/__init__.py287
-rw-r--r--nikola/plugins/compile_bbcode.py16
-rw-r--r--nikola/plugins/compile_html.py14
-rw-r--r--nikola/plugins/compile_ipynb.plugin10
-rw-r--r--nikola/plugins/compile_ipynb/README.txt35
-rw-r--r--nikola/plugins/compile_ipynb/__init__.py100
-rw-r--r--nikola/plugins/compile_markdown/__init__.py51
-rw-r--r--nikola/plugins/compile_markdown/mdx_gist.py189
-rw-r--r--nikola/plugins/compile_markdown/mdx_nikola.py56
-rw-r--r--nikola/plugins/compile_markdown/mdx_podcast.py87
-rw-r--r--nikola/plugins/compile_misaka.plugin10
-rw-r--r--nikola/plugins/compile_misaka/__init__.py82
-rw-r--r--nikola/plugins/compile_rest/__init__.py81
-rw-r--r--nikola/plugins/compile_rest/dummy.py44
-rw-r--r--nikola/plugins/compile_rest/gist_directive.py2
-rw-r--r--nikola/plugins/compile_rest/listing.py121
-rw-r--r--nikola/plugins/compile_rest/pygments_code_block_directive.py424
-rw-r--r--nikola/plugins/compile_rest/slides.py79
-rw-r--r--nikola/plugins/compile_rest/soundcloud.py62
-rw-r--r--nikola/plugins/compile_rest/vimeo.py114
-rw-r--r--nikola/plugins/compile_rest/youtube.py57
-rw-r--r--nikola/plugins/compile_textile.py14
-rw-r--r--nikola/plugins/compile_txt2tags.py14
-rw-r--r--nikola/plugins/compile_wiki.py12
-rw-r--r--nikola/plugins/task_archive.py66
-rw-r--r--nikola/plugins/task_copy_assets.py25
-rw-r--r--nikola/plugins/task_create_bundles.py46
-rw-r--r--nikola/plugins/task_indexes.py35
-rw-r--r--nikola/plugins/task_localsearch.plugin10
-rw-r--r--nikola/plugins/task_localsearch/MIT-LICENSE.txt20
-rw-r--r--nikola/plugins/task_localsearch/__init__.py102
-rwxr-xr-xnikola/plugins/task_localsearch/files/assets/css/img/expand.pngbin0 -> 424 bytes
-rwxr-xr-xnikola/plugins/task_localsearch/files/assets/css/img/link.pngbin0 -> 463 bytes
-rw-r--r--nikola/plugins/task_localsearch/files/assets/css/img/loader.gifbin0 -> 4178 bytes
-rw-r--r--nikola/plugins/task_localsearch/files/assets/css/img/search.gifbin0 -> 208 bytes
-rwxr-xr-xnikola/plugins/task_localsearch/files/assets/css/tipuesearch.css232
-rw-r--r--nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js426
-rw-r--r--nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js28
-rwxr-xr-xnikola/plugins/task_localsearch/files/tipue_search.html31
-rw-r--r--nikola/plugins/task_mustache.plugin10
-rw-r--r--nikola/plugins/task_mustache/__init__.py197
-rw-r--r--nikola/plugins/task_mustache/mustache-template.html29
-rw-r--r--nikola/plugins/task_mustache/mustache.html36
-rw-r--r--nikola/plugins/task_redirect.py2
-rw-r--r--nikola/plugins/task_render_galleries.py29
-rw-r--r--nikola/plugins/task_render_listings.py12
-rw-r--r--nikola/plugins/task_render_pages.py5
-rw-r--r--nikola/plugins/task_render_posts.py84
-rw-r--r--nikola/plugins/task_render_rss.py11
-rw-r--r--nikola/plugins/task_render_sources.py21
-rw-r--r--nikola/plugins/task_render_tags.py20
-rw-r--r--nikola/plugins/task_sitemap/__init__.py94
-rw-r--r--nikola/plugins/task_sitemap/sitemap_gen.py2137
61 files changed, 2901 insertions, 2989 deletions
diff --git a/nikola/plugins/command_check.py b/nikola/plugins/command_check.py
index a396f63..ea82703 100644
--- a/nikola/plugins/command_check.py
+++ b/nikola/plugins/command_check.py
@@ -24,6 +24,7 @@
from __future__ import print_function
import os
+import sys
try:
from urllib import unquote
from urlparse import urlparse
@@ -74,14 +75,17 @@ class CommandCheck(Command):
print(self.help())
return False
if options['links']:
- scan_links(options['find_sources'])
+ failure = scan_links(options['find_sources'])
if options['files']:
- scan_files()
+ failure = scan_files()
+ if failure:
+ sys.exit(1)
existing_targets = set([])
def analize(task, find_sources=False):
+ rv = False
try:
filename = task.split(":")[-1]
d = lxml.html.fromstring(open(filename).read())
@@ -100,6 +104,7 @@ def analize(task, find_sources=False):
if os.path.exists(target_filename):
existing_targets.add(target_filename)
else:
+ rv = True
print("Broken link in {0}: ".format(filename), target)
if find_sources:
print("Possible sources:")
@@ -109,17 +114,21 @@ def analize(task, find_sources=False):
except Exception as exc:
print("Error with:", filename, exc)
+ return rv
def scan_links(find_sources=False):
print("Checking Links:\n===============\n")
+ failure = False
for task in os.popen('nikola list --all', 'r').readlines():
task = task.strip()
if task.split(':')[0] in ('render_tags', 'render_archive',
'render_galleries', 'render_indexes',
- 'render_pages',
+ 'render_pages'
'render_site') and '.html' in task:
- analize(task, find_sources)
+ if analize(task, find_sources):
+ failure = True
+ return failure
def scan_files():
@@ -127,6 +136,7 @@ def scan_files():
task_fnames = set([])
real_fnames = set([])
# First check that all targets are generated in the right places
+ failure = False
for task in os.popen('nikola list --all', 'r').readlines():
task = task.strip()
if 'output' in task and ':' in task:
@@ -144,6 +154,7 @@ def scan_files():
print("\nFiles from unknown origins:\n")
for f in only_on_output:
print(f)
+ failure = True
only_on_input = list(task_fnames - real_fnames)
if only_on_input:
@@ -151,3 +162,5 @@ def scan_files():
print("\nFiles not generated:\n")
for f in only_on_input:
print(f)
+
+ return failure
diff --git a/nikola/plugins/command_console.py b/nikola/plugins/command_console.py
index 4af759f..f4d0295 100644
--- a/nikola/plugins/command_console.py
+++ b/nikola/plugins/command_console.py
@@ -29,35 +29,77 @@ import os
from nikola.plugin_categories import Command
-class Deploy(Command):
+class Console(Command):
"""Start debugging console."""
name = "console"
+ shells = ['ipython', 'bpython', 'plain']
+ doc_purpose = "Start an interactive python console with access to your site and configuration."
- def _execute(self, options, args):
- """Start the console."""
+ def ipython(self):
+ """IPython shell."""
from nikola import Nikola
try:
import conf
+ except ImportError:
+ print("No configuration found, cannot run the console.")
+ else:
+ import IPython
SITE = Nikola(**conf.__dict__)
SITE.scan_posts()
- print("You can now access your configuration as conf and your "
- "site engine as SITE.")
+ IPython.embed(header='Nikola Console (conf = configuration, SITE '
+ '= site engine)')
+
+ def bpython(self):
+ """bpython shell."""
+ from nikola import Nikola
+ try:
+ import conf
except ImportError:
- print("No configuration found.")
- import code
+ print("No configuration found, cannot run the console.")
+ else:
+ import bpython
+ SITE = Nikola(**conf.__dict__)
+ SITE.scan_posts()
+ gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola}
+ bpython.embed(banner='Nikola Console (conf = configuration, SITE '
+ '= site engine)', locals_=gl)
+
+ def plain(self):
+ """Plain Python shell."""
+ from nikola import Nikola
try:
- import readline
+ import conf
+ SITE = Nikola(**conf.__dict__)
+ SITE.scan_posts()
+ gl = {'conf': conf, 'SITE': SITE, 'Nikola': Nikola}
except ImportError:
- pass
+ print("No configuration found, cannot run the console.")
else:
- import rlcompleter
- readline.set_completer(rlcompleter.Completer(globals()).complete)
- readline.parse_and_bind("tab:complete")
+ import code
+ try:
+ import readline
+ except ImportError:
+ pass
+ else:
+ import rlcompleter
+ readline.set_completer(rlcompleter.Completer(gl).complete)
+ readline.parse_and_bind("tab:complete")
+
+ pythonrc = os.environ.get("PYTHONSTARTUP")
+ if pythonrc and os.path.isfile(pythonrc):
+ try:
+ execfile(pythonrc) # NOQA
+ except NameError:
+ pass
+
+ code.interact(local=gl, banner='Nikola Console (conf = '
+ 'configuration, SITE = site engine)')
- pythonrc = os.environ.get("PYTHONSTARTUP")
- if pythonrc and os.path.isfile(pythonrc):
+ def _execute(self, options, args):
+ """Start the console."""
+ for shell in self.shells:
try:
- execfile(pythonrc) # NOQA
- except NameError:
+ return getattr(self, shell)()
+ except ImportError:
pass
- code.interact(local=globals())
+ raise ImportError
diff --git a/nikola/plugins/command_deploy.py b/nikola/plugins/command_deploy.py
index ffa86ab..3277567 100644
--- a/nikola/plugins/command_deploy.py
+++ b/nikola/plugins/command_deploy.py
@@ -23,7 +23,12 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from __future__ import print_function
+from ast import literal_eval
+import codecs
+from datetime import datetime
import os
+import subprocess
+
from nikola.plugin_categories import Command
@@ -37,5 +42,24 @@ class Deploy(Command):
def _execute(self, command, args):
for command in self.site.config['DEPLOY_COMMANDS']:
+
+ # Get last succesful deploy date
+ timestamp_path = os.path.join(self.site.config['CACHE_FOLDER'], 'lastdeploy')
+ try:
+ with open(timestamp_path, 'rb') as inf:
+ last_deploy = literal_eval(inf.read().strip())
+ except Exception:
+ last_deploy = datetime(1970, 1, 1) # NOQA
+
print("==>", command)
- os.system(command)
+ ret = subprocess.check_call(command, shell=True)
+ if ret != 0: # failed deployment
+ raise Exception("Failed deployment")
+ print("Successful deployment")
+ new_deploy = datetime.now()
+ # Store timestamp of successful deployment
+ with codecs.open(timestamp_path, 'wb+', 'utf8') as outf:
+ outf.write(repr(new_deploy))
+ # Here is where we would do things with whatever is
+ # on self.site.timeline and is newer than
+ # last_deploy
diff --git a/nikola/plugins/command_import_blogger.py b/nikola/plugins/command_import_blogger.py
index 35a702e..ecc4676 100644
--- a/nikola/plugins/command_import_blogger.py
+++ b/nikola/plugins/command_import_blogger.py
@@ -73,7 +73,7 @@ class CommandImportBlogger(Command):
]
def _execute(self, options, args):
- """Import a Wordpress blog from an export file into a Nikola site."""
+ """Import a Blogger blog from an export file into a Nikola site."""
# Parse the data
if feedparser is None:
@@ -126,7 +126,7 @@ class CommandImportBlogger(Command):
def generate_base_site(self):
if not os.path.exists(self.output_folder):
- os.system('nikola init --empty ' + self.output_folder)
+ os.system('nikola init ' + self.output_folder)
else:
self.import_into_existing_site = True
print('The folder {0} already exists - assuming that this is a '
@@ -176,9 +176,16 @@ class CommandImportBlogger(Command):
@staticmethod
def write_metadata(filename, title, slug, post_date, description, tags):
+ if not description:
+ description = ""
+
with codecs.open(filename, "w+", "utf8") as fd:
- fd.write('\n'.join((title, slug, post_date, ','.join(tags), '',
- description)))
+ fd.write('{0}\n'.format(title))
+ fd.write('{0}\n'.format(slug))
+ fd.write('{0}\n'.format(post_date))
+ fd.write('{0}\n'.format(','.join(tags)))
+ fd.write('\n')
+ fd.write('{0}\n'.format(description))
def import_item(self, item, out_folder=None):
"""Takes an item from the feed and creates a post file."""
@@ -284,7 +291,7 @@ class CommandImportBlogger(Command):
if not self.import_into_existing_site:
filename = 'conf.py'
else:
- filename = 'conf.py.wordpress_import-{0}'.format(
+ filename = 'conf.py.blogger_import-{0}'.format(
datetime.datetime.now().strftime('%Y%m%d_%H%M%s'))
config_output_path = os.path.join(self.output_folder, filename)
print('Configuration will be written to: ' + config_output_path)
diff --git a/nikola/plugins/command_import_wordpress.py b/nikola/plugins/command_import_wordpress.py
index e7ecca0..b45fe78 100644
--- a/nikola/plugins/command_import_wordpress.py
+++ b/nikola/plugins/command_import_wordpress.py
@@ -90,7 +90,6 @@ class CommandImportWordpress(Command):
def _execute(self, options={}, args=[]):
"""Import a Wordpress blog from an export file into a Nikola site."""
# Parse the data
- print(options, args)
if requests is None:
print('To use the import_wordpress command,'
' you have to install the "requests" package.')
@@ -100,10 +99,16 @@ class CommandImportWordpress(Command):
print(self.help())
return
- options['filename'] = args[0]
+ options['filename'] = args.pop(0)
- if len(args) > 1:
- options['output_folder'] = args[1]
+ if args and ('output_folder' not in args or
+ options['output_folder'] == 'new_site'):
+ options['output_folder'] = args.pop(0)
+
+ if args:
+ print('You specified additional arguments ({0}). Please consider '
+ 'putting these arguments before the filename if you '
+ 'are running into problems.'.format(args))
self.wordpress_export_file = options['filename']
self.squash_newlines = options.get('squash_newlines', False)
@@ -204,8 +209,12 @@ class CommandImportWordpress(Command):
'PUT TITLE HERE')
context['BLOG_DESCRIPTION'] = get_text_tag(
channel, 'description', 'PUT DESCRIPTION HERE')
- context['SITE_URL'] = get_text_tag(channel, 'link', '#')
context['BASE_URL'] = get_text_tag(channel, 'link', '#')
+ if not context['BASE_URL']:
+ base_site_url = channel.find('{{{0}}}author'.format(wordpress_namespace))
+ context['BASE_URL'] = get_text_tag(base_site_url, None, "http://foo.com")
+ context['SITE_URL'] = context['BASE_URL']
+
author = channel.find('{{{0}}}author'.format(wordpress_namespace))
context['BLOG_EMAIL'] = get_text_tag(
author,
@@ -314,7 +323,13 @@ class CommandImportWordpress(Command):
# link is something like http://foo.com/2012/09/01/hello-world/
# So, take the path, utils.slugify it, and that's our slug
link = get_text_tag(item, 'link', None)
- slug = utils.slugify(urlparse(link).path)
+ path = urlparse(link).path
+
+ # In python 2, path is a str. slug requires a unicode
+ # object. Luckily, paths are also ASCII
+ if isinstance(path, utils.bytes_str):
+ path = path.decode('ASCII')
+ slug = utils.slugify(path)
if not slug: # it happens if the post has no "nice" URL
slug = get_text_tag(
item, '{{{0}}}post_name'.format(wordpress_namespace), None)
@@ -334,7 +349,10 @@ class CommandImportWordpress(Command):
item, '{http://purl.org/rss/1.0/modules/content/}encoded', '')
tags = []
- if status != 'publish':
+ if status == 'trash':
+ print('Trashed post "{0}" will not be imported.'.format(title))
+ return
+ elif status != 'publish':
tags.append('draft')
is_draft = True
else:
diff --git a/nikola/plugins/command_install_theme.py b/nikola/plugins/command_install_theme.py
index 04a2cce..2a0a0cc 100644
--- a/nikola/plugins/command_install_theme.py
+++ b/nikola/plugins/command_install_theme.py
@@ -64,6 +64,10 @@ class CommandInstallTheme(Command):
def _execute(self, options, args):
"""Install theme into current site."""
+ if requests is None:
+ print('This command requires the requests package be installed.')
+ return False
+
listing = options['list']
url = options['url']
if args:
diff --git a/nikola/plugins/command_new_post.py b/nikola/plugins/command_new_post.py
index a823da3..933a51a 100644
--- a/nikola/plugins/command_new_post.py
+++ b/nikola/plugins/command_new_post.py
@@ -49,13 +49,31 @@ def filter_post_pages(compiler, is_post, post_compilers, post_pages):
if not filtered:
type_name = "post" if is_post else "page"
- raise Exception("Can't find a way, using your configuration, to create"
+ raise Exception("Can't find a way, using your configuration, to create "
"a {0} in format {1}. You may want to tweak "
"post_compilers or post_pages in conf.py".format(
type_name, compiler))
return filtered[0]
+def get_default_compiler(is_post, post_compilers, post_pages):
+ """Given post_compilers and post_pages, return a reasonable
+ default compiler for this kind of post/page.
+ """
+
+ # First throw away all the post_pages with the wrong is_post
+ filtered = [entry for entry in post_pages if entry[3] == is_post]
+
+ # Get extensions in filtered post_pages until one matches a compiler
+ for entry in filtered:
+ extension = os.path.splitext(entry[0])[-1]
+ for compiler, extensions in post_compilers.items():
+ if extension in extensions:
+ return compiler
+ # No idea, back to default behaviour
+ return 'rest'
+
+
class CommandNewPost(Command):
"""Create a new post."""
@@ -105,7 +123,7 @@ class CommandNewPost(Command):
'short': 'f',
'long': 'format',
'type': str,
- 'default': 'rest',
+ 'default': '',
'help': 'Markup format for post, one of rest, markdown, wiki, '
'bbcode, html, textile, txt2tags',
}
@@ -140,6 +158,12 @@ class CommandNewPost(Command):
post_format = options['post_format']
+ if not post_format: # Issue #400
+ post_format = get_default_compiler(
+ is_post,
+ self.site.config['post_compilers'],
+ self.site.config['post_pages'])
+
if post_format not in compiler_names:
print("ERROR: Unknown post format " + post_format)
return
@@ -160,12 +184,14 @@ class CommandNewPost(Command):
title = sys.stdin.readline()
else:
print("Title:", title)
- if isinstance(title, bytes):
+ if isinstance(title, utils.bytes_str):
title = title.decode(sys.stdin.encoding)
title = title.strip()
if not path:
slug = utils.slugify(title)
else:
+ if isinstance(path, utils.bytes_str):
+ path = path.decode(sys.stdin.encoding)
slug = utils.slugify(os.path.splitext(os.path.basename(path))[0])
date = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
data = [title, slug, date, tags]
@@ -186,7 +212,9 @@ class CommandNewPost(Command):
d_name = os.path.dirname(txt_path)
if not os.path.exists(d_name):
os.makedirs(d_name)
- compiler_plugin.create_post(txt_path, onefile, title, slug, date, tags)
+ compiler_plugin.create_post(
+ txt_path, onefile, title=title,
+ slug=slug, date=date, tags=tags)
if not onefile: # write metadata file
with codecs.open(meta_path, "wb+", "utf8") as fd:
diff --git a/nikola/plugins/command_planetoid.plugin b/nikola/plugins/command_planetoid.plugin
new file mode 100644
index 0000000..8636d49
--- /dev/null
+++ b/nikola/plugins/command_planetoid.plugin
@@ -0,0 +1,9 @@
+[Core]
+Name = planetoid
+Module = command_planetoid
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Maintain a planet-like site
diff --git a/nikola/plugins/command_planetoid/__init__.py b/nikola/plugins/command_planetoid/__init__.py
new file mode 100644
index 0000000..183dd51
--- /dev/null
+++ b/nikola/plugins/command_planetoid/__init__.py
@@ -0,0 +1,287 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import print_function, unicode_literals
+import codecs
+import datetime
+import hashlib
+from optparse import OptionParser
+import os
+import sys
+
+from doit.tools import timeout
+from nikola.plugin_categories import Command, Task
+from nikola.utils import config_changed
+
+try:
+ import feedparser
+except ImportError:
+ feedparser = None # NOQA
+
+try:
+ import peewee
+except ImportError:
+ peewee = None
+
+
+if peewee is not None:
+ class Feed(peewee.Model):
+ name = peewee.CharField()
+ url = peewee.CharField(max_length=200)
+ last_status = peewee.CharField(null=True)
+ etag = peewee.CharField(max_length=200)
+ last_modified = peewee.DateTimeField()
+
+ class Entry(peewee.Model):
+ date = peewee.DateTimeField()
+ feed = peewee.ForeignKeyField(Feed)
+ content = peewee.TextField(max_length=20000)
+ link = peewee.CharField(max_length=200)
+ title = peewee.CharField(max_length=200)
+ guid = peewee.CharField(max_length=200)
+
+
+class Planetoid(Command, Task):
+ """Maintain a planet-like thing."""
+ name = "planetoid"
+
+ def init_db(self):
+ # setup database
+ Feed.create_table(fail_silently=True)
+ Entry.create_table(fail_silently=True)
+
+ def gen_tasks(self):
+ if peewee is None or sys.version_info[0] == 3:
+ if sys.version_info[0] == 3:
+ message = 'Peewee is currently incompatible with Python 3.'
+ else:
+ message = 'You need to install the \"peewee\" module.'
+
+ yield {
+ 'basename': self.name,
+ 'name': '',
+ 'verbosity': 2,
+ 'actions': ['echo "%s"' % message]
+ }
+ else:
+ self.init_db()
+ self.load_feeds()
+ for task in self.task_update_feeds():
+ yield task
+ for task in self.task_generate_posts():
+ yield task
+ yield {
+ 'basename': self.name,
+ 'name': '',
+ 'actions': [],
+ 'file_dep': ['feeds'],
+ 'task_dep': [
+ self.name + "_fetch_feed",
+ self.name + "_generate_posts",
+ ]
+ }
+
+ def run(self, *args):
+ parser = OptionParser(usage="nikola %s [options]" % self.name)
+ (options, args) = parser.parse_args(list(args))
+
+ def load_feeds(self):
+ "Read the feeds file, add it to the database."
+ feeds = []
+ feed = name = None
+ for line in codecs.open('feeds', 'r', 'utf-8'):
+ line = line.strip()
+ if line.startswith("#"):
+ continue
+ elif line.startswith('http'):
+ feed = line
+ elif line:
+ name = line
+ if feed and name:
+ feeds.append([feed, name])
+ feed = name = None
+
+ def add_feed(name, url):
+ f = Feed.create(
+ name=name,
+ url=url,
+ etag='foo',
+ last_modified=datetime.datetime(1970, 1, 1),
+ )
+ f.save()
+
+ def update_feed_url(feed, url):
+ feed.url = url
+ feed.save()
+
+ for feed, name in feeds:
+ f = Feed.select().where(Feed.name == name)
+ if not list(f):
+ add_feed(name, feed)
+ elif list(f)[0].url != feed:
+ update_feed_url(list(f)[0], feed)
+
+ def task_update_feeds(self):
+ """Download feed contents, add entries to the database."""
+ def update_feed(feed):
+ modified = feed.last_modified.timetuple()
+ etag = feed.etag
+ try:
+ parsed = feedparser.parse(
+ feed.url,
+ etag=etag,
+ modified=modified
+ )
+ feed.last_status = str(parsed.status)
+ except: # Probably a timeout
+ # TODO: log failure
+ return
+ if parsed.feed.get('title'):
+ print(parsed.feed.title)
+ else:
+ print(feed.url)
+ feed.etag = parsed.get('etag', 'foo')
+ modified = tuple(parsed.get('date_parsed', (1970, 1, 1)))[:6]
+ print("==========>", modified)
+ modified = datetime.datetime(*modified)
+ feed.last_modified = modified
+ feed.save()
+ # No point in adding items from missinfg feeds
+ if parsed.status > 400:
+ # TODO log failure
+ return
+ for entry_data in parsed.entries:
+ print("=========================================")
+ date = entry_data.get('published_parsed', None)
+ if date is None:
+ date = entry_data.get('updated_parsed', None)
+ if date is None:
+ print("Can't parse date from:")
+ print(entry_data)
+ return False
+ print("DATE:===>", date)
+ date = datetime.datetime(*(date[:6]))
+ title = "%s: %s" % (feed.name, entry_data.get('title', 'Sin título'))
+ content = entry_data.get('content', None)
+ if content:
+ content = content[0].value
+ if not content:
+ content = entry_data.get('description', None)
+ if not content:
+ content = entry_data.get('summary', 'Sin contenido')
+ guid = str(entry_data.get('guid', entry_data.link))
+ link = entry_data.link
+ print(repr([date, title]))
+ e = list(Entry.select().where(Entry.guid == guid))
+ print(
+ repr(dict(
+ date=date,
+ title=title,
+ content=content,
+ guid=guid,
+ feed=feed,
+ link=link,
+ ))
+ )
+ if not e:
+ entry = Entry.create(
+ date=date,
+ title=title,
+ content=content,
+ guid=guid,
+ feed=feed,
+ link=link,
+ )
+ else:
+ entry = e[0]
+ entry.date = date
+ entry.title = title
+ entry.content = content
+ entry.link = link
+ entry.save()
+ flag = False
+ for feed in Feed.select():
+ flag = True
+ task = {
+ 'basename': self.name + "_fetch_feed",
+ 'name': str(feed.url),
+ 'actions': [(update_feed, (feed, ))],
+ 'uptodate': [timeout(datetime.timedelta(minutes=
+ self.site.config.get('PLANETOID_REFRESH', 60)))],
+ }
+ yield task
+ if not flag:
+ yield {
+ 'basename': self.name + "_fetch_feed",
+ 'name': '',
+ 'actions': [],
+ }
+
+ def task_generate_posts(self):
+ """Generate post files for the blog entries."""
+ def gen_id(entry):
+ h = hashlib.md5()
+ h.update(entry.feed.name.encode('utf8'))
+ h.update(entry.guid)
+ return h.hexdigest()
+
+ def generate_post(entry):
+ unique_id = gen_id(entry)
+ meta_path = os.path.join('posts', unique_id + '.meta')
+ post_path = os.path.join('posts', unique_id + '.txt')
+ with codecs.open(meta_path, 'wb+', 'utf8') as fd:
+ fd.write('%s\n' % entry.title.replace('\n', ' '))
+ fd.write('%s\n' % unique_id)
+ fd.write('%s\n' % entry.date.strftime('%Y/%m/%d %H:%M'))
+ fd.write('\n')
+ fd.write('%s\n' % entry.link)
+ with codecs.open(post_path, 'wb+', 'utf8') as fd:
+ fd.write('.. raw:: html\n\n')
+ content = entry.content
+ if not content:
+ content = 'Sin contenido'
+ for line in content.splitlines():
+ fd.write(' %s\n' % line)
+
+ if not os.path.isdir('posts'):
+ os.mkdir('posts')
+ flag = False
+ for entry in Entry.select().order_by(Entry.date.desc()):
+ flag = True
+ entry_id = gen_id(entry)
+ yield {
+ 'basename': self.name + "_generate_posts",
+ 'targets': [os.path.join('posts', entry_id + '.meta'), os.path.join('posts', entry_id + '.txt')],
+ 'name': entry_id,
+ 'actions': [(generate_post, (entry,))],
+ 'uptodate': [config_changed({1: entry})],
+ 'task_dep': [self.name + "_fetch_feed"],
+ }
+ if not flag:
+ yield {
+ 'basename': self.name + "_generate_posts",
+ 'name': '',
+ 'actions': [],
+ }
diff --git a/nikola/plugins/compile_bbcode.py b/nikola/plugins/compile_bbcode.py
index 26de727..f8022f3 100644
--- a/nikola/plugins/compile_bbcode.py
+++ b/nikola/plugins/compile_bbcode.py
@@ -60,19 +60,17 @@ class CompileTextile(PageCompiler):
output = self.parser.format(data)
out_file.write(output)
- def create_post(self, path, onefile=False, title="", slug="", date="",
- tags=""):
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
d_name = os.path.dirname(path)
if not os.path.isdir(d_name):
os.makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('[note]<!--\n')
- fd.write('.. title: {0}\n'.format(title))
- fd.write('.. slug: {0}\n'.format(slug))
- fd.write('.. date: {0}\n'.format(date))
- fd.write('.. tags: {0}\n'.format(tags))
- fd.write('.. link: \n')
- fd.write('.. description: \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
fd.write('-->[/note]\n\n')
- fd.write("\nWrite your post here.")
+ fd.write("Write your post here.")
diff --git a/nikola/plugins/compile_html.py b/nikola/plugins/compile_html.py
index 6c1c381..7551b33 100644
--- a/nikola/plugins/compile_html.py
+++ b/nikola/plugins/compile_html.py
@@ -43,19 +43,17 @@ class CompileHtml(PageCompiler):
pass
shutil.copyfile(source, dest)
- def create_post(self, path, onefile=False, title="", slug="",
- date="", tags=""):
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
d_name = os.path.dirname(path)
if not os.path.isdir(d_name):
os.makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('<!-- \n')
- fd.write('.. title: {0}\n'.format(title))
- fd.write('.. slug: {0}\n'.format(slug))
- fd.write('.. date: {0}\n'.format(date))
- fd.write('.. tags: {0}\n'.format(tags))
- fd.write('.. link: \n')
- fd.write('.. description: \n')
+ for k, v in metadata.keys():
+ fd.write('.. {0}: {1}\n'.format(k, v))
fd.write('-->\n\n')
fd.write("\n<p>Write your post here.</p>")
diff --git a/nikola/plugins/compile_ipynb.plugin b/nikola/plugins/compile_ipynb.plugin
new file mode 100644
index 0000000..51051e0
--- /dev/null
+++ b/nikola/plugins/compile_ipynb.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = ipynb
+Module = compile_ipynb
+
+[Documentation]
+Author = Damián Avila
+Version = 0.1
+Website = http://www.oquanta.info
+Description = Compile IPython notebooks into HTML
+
diff --git a/nikola/plugins/compile_ipynb/README.txt b/nikola/plugins/compile_ipynb/README.txt
new file mode 100644
index 0000000..2cfd45e
--- /dev/null
+++ b/nikola/plugins/compile_ipynb/README.txt
@@ -0,0 +1,35 @@
+To make this work...
+
+1- First, you have to put this plugin in your_site/plugins/ folder.
+
+2- Then, you have to download the custom nbconvert from here: https://github.com/damianavila/compile_ipynb-for-Nikola.git
+and put it inside your_site/plugins/compile_ipynb/ folder
+
+3- Also, you have to use the site-ipython theme (or make a new one containing the ipython css, mathjax.js and the proper template).
+You can get it here: https://github.com/damianavila/site-ipython-theme-for-Nikola
+
+4- Finally, you have to put:
+
+post_pages = (
+ ("posts/*.ipynb", "posts", "post.tmpl", True),
+ ("stories/*.ipynb", "stories", "story.tmpl", False),
+)
+
+in your conf.py
+
+Then... to use it:
+
+$nikola new_page -f ipynb
+
+**NOTE**: Just IGNORE the "-1" and "-2" options in nikola new_page command, by default this compiler
+create one metadata file and the corresponding naive IPython notebook.
+
+$nikola build
+
+And deploy the output folder... to see it locally: $nikola serve
+
+If you have any doubts, just ask: @damianavila
+
+Cheers.
+
+Damián
diff --git a/nikola/plugins/compile_ipynb/__init__.py b/nikola/plugins/compile_ipynb/__init__.py
new file mode 100644
index 0000000..d38f6f2
--- /dev/null
+++ b/nikola/plugins/compile_ipynb/__init__.py
@@ -0,0 +1,100 @@
+# Copyright (c) 2013 Damian Avila.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Implementation of compile_html based on nbconvert."""
+
+from __future__ import unicode_literals, print_function
+import codecs
+import os
+
+try:
+ from .nbformat import current as nbformat
+ from .nbconvert.converters import bloggerhtml as nbconverter
+ bloggerhtml = True
+except ImportError:
+ bloggerhtml = None
+
+from nikola.plugin_categories import PageCompiler
+
+
+class CompileIPynb(PageCompiler):
+ """Compile IPynb into HTML."""
+
+ name = "ipynb"
+
+ def compile_html(self, source, dest):
+ if bloggerhtml is None:
+ raise Exception('To build this site, you also need '
+ 'https://github.com/damianavila/com'
+ 'pile_ipynb-for-Nikola.git.')
+ try:
+ os.makedirs(os.path.dirname(dest))
+ except:
+ pass
+ converter = nbconverter.ConverterBloggerHTML()
+ with codecs.open(dest, "w+", "utf8") as out_file:
+ with codecs.open(source, "r", "utf8") as in_file:
+ data = in_file.read()
+ converter.nb = nbformat.reads_json(data)
+ output = converter.convert()
+ out_file.write(output)
+
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+ d_name = os.path.dirname(path)
+ if not os.path.isdir(d_name):
+ os.makedirs(os.path.dirname(path))
+ meta_path = os.path.join(d_name, kw['slug'] + ".meta")
+ with codecs.open(meta_path, "wb+", "utf8") as fd:
+ if onefile:
+ fd.write('%s\n' % kw['title'])
+ fd.write('%s\n' % kw['slug'])
+ fd.write('%s\n' % kw['date'])
+ fd.write('%s\n' % kw['tags'])
+ print("Your post's metadata is at: ", meta_path)
+ with codecs.open(path, "wb+", "utf8") as fd:
+ fd.write("""{
+ "metadata": {
+ "name": "%s"
+ },
+ "nbformat": 3,
+ "nbformat_minor": 0,
+ "worksheets": [
+ {
+ "cells": [
+ {
+ "cell_type": "code",
+ "collapsed": false,
+ "input": [],
+ "language": "python",
+ "metadata": {},
+ "outputs": []
+ }
+ ],
+ "metadata": {}
+ }
+ ]
+}""" % kw['slug'])
diff --git a/nikola/plugins/compile_markdown/__init__.py b/nikola/plugins/compile_markdown/__init__.py
index 7aa03a9..ae700e6 100644
--- a/nikola/plugins/compile_markdown/__init__.py
+++ b/nikola/plugins/compile_markdown/__init__.py
@@ -24,14 +24,28 @@
"""Implementation of compile_html based on markdown."""
+from __future__ import unicode_literals
+
import codecs
import os
-import re
try:
from markdown import markdown
+
+ from nikola.plugins.compile_markdown.mdx_nikola import NikolaExtension
+ nikola_extension = NikolaExtension()
+
+ from nikola.plugins.compile_markdown.mdx_gist import GistExtension
+ gist_extension = GistExtension()
+
+ from nikola.plugins.compile_markdown.mdx_podcast import PodcastExtension
+ podcast_extension = PodcastExtension()
+
except ImportError:
markdown = None # NOQA
+ nikola_extension = None
+ gist_extension = None
+ podcast_extension = None
from nikola.plugin_categories import PageCompiler
@@ -41,6 +55,9 @@ class CompileMarkdown(PageCompiler):
name = "markdown"
+ extensions = ['fenced_code', 'codehilite', gist_extension,
+ nikola_extension, podcast_extension]
+
def compile_html(self, source, dest):
if markdown is None:
raise Exception('To build this site, you need to install the '
@@ -52,30 +69,20 @@ class CompileMarkdown(PageCompiler):
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
data = in_file.read()
- output = markdown(data, ['fenced_code', 'codehilite'])
- # h1 is reserved for the title so increment all header levels
- for n in reversed(range(1, 9)):
- output = re.sub('<h{0}>'.format(n), '<h{0}>'.format(n + 1),
- output)
- output = re.sub('</h{0}>'.format(n), '</h{0}>'.format(n + 1),
- output)
- # python-markdown's highlighter uses the class 'codehilite' to wrap
- # code, # instead of the standard 'code'. None of the standard
- # pygments stylesheets use this class, so swap it to be 'code'
- output = re.sub(r'(<div[^>]+class="[^"]*)codehilite([^>]+)',
- r'\1code\2', output)
+ output = markdown(data, self.extensions)
out_file.write(output)
- def create_post(self, path, onefile=False, title="", slug="", date="",
- tags=""):
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+ d_name = os.path.dirname(path)
+ if not os.path.isdir(d_name):
+ os.makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('<!-- \n')
- fd.write('.. title: {0}\n'.format(title))
- fd.write('.. slug: {0}\n'.format(slug))
- fd.write('.. date: {0}\n'.format(date))
- fd.write('.. tags: {0}\n'.format(tags))
- fd.write('.. link: \n')
- fd.write('.. description: \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
fd.write('-->\n\n')
- fd.write("\nWrite your post here.")
+ fd.write("Write your post here.")
diff --git a/nikola/plugins/compile_markdown/mdx_gist.py b/nikola/plugins/compile_markdown/mdx_gist.py
new file mode 100644
index 0000000..808e383
--- /dev/null
+++ b/nikola/plugins/compile_markdown/mdx_gist.py
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2013 Michael Rabbitt.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+# Inspired by "[Python] reStructuredText GitHub Gist directive"
+# (https://gist.github.com/brianhsu/1407759), public domain by Brian Hsu
+
+from __future__ import print_function
+
+
+'''
+Extension to Python Markdown for Embedded Gists (gist.github.com)
+
+Basic Example:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... [:gist: 4747847]
+ ... """
+ >>> html = markdown.markdown(text, [GistExtension()])
+ >>> print(html)
+ <p>Text of the gist:
+ <div class="gist">
+ <script src="https://gist.github.com/4747847.js"></script>
+ <noscript>
+ <pre>import this</pre>
+ </noscript>
+ </div>
+ </p>
+
+Example with filename:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... [:gist: 4747847 zen.py]
+ ... """
+ >>> html = markdown.markdown(text, [GistExtension()])
+ >>> print(html)
+ <p>Text of the gist:
+ <div class="gist">
+ <script src="https://gist.github.com/4747847.js?file=zen.py"></script>
+ <noscript>
+ <pre>import this</pre>
+ </noscript>
+ </div>
+ </p>
+
+Example using reStructuredText syntax:
+
+ >>> import markdown
+ >>> text = """
+ ... Text of the gist:
+ ... .. gist:: 4747847 zen.py
+ ... """
+ >>> html = markdown.markdown(text, [GistExtension()])
+ >>> print(html)
+ <p>Text of the gist:
+ <div class="gist">
+ <script src="https://gist.github.com/4747847.js?file=zen.py"></script>
+ <noscript>
+ <pre>import this</pre>
+ </noscript>
+ </div>
+ </p>
+'''
+from __future__ import unicode_literals
+import warnings
+from markdown.extensions import Extension
+from markdown.inlinepatterns import Pattern
+from markdown.util import AtomicString
+from markdown.util import etree
+
+try:
+ import requests
+except ImportError:
+ requests = None # NOQA
+
+GIST_JS_URL = "https://gist.github.com/{0}.js"
+GIST_FILE_JS_URL = "https://gist.github.com/{0}.js?file={1}"
+GIST_RAW_URL = "https://raw.github.com/gist/{0}"
+GIST_FILE_RAW_URL = "https://raw.github.com/gist/{0}/{1}"
+
+GIST_MD_RE = r'\[:gist:\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+?))?\]'
+GIST_RST_RE = r'(?m)^\.\.\s*gist::\s*(?P<gist_id>\d+)(?:\s*(?P<filename>.+))\s*$'
+
+
+class GistPattern(Pattern):
+ """ InlinePattern for footnote markers in a document's body text. """
+
+ def __init__(self, pattern, configs):
+ Pattern.__init__(self, pattern)
+
+ def get_raw_gist_with_filename(self, gist_id, filename):
+ url = GIST_FILE_RAW_URL.format(gist_id, filename)
+ return requests.get(url).text
+
+ def get_raw_gist(self, gist_id):
+ url = GIST_RAW_URL.format(gist_id)
+ return requests.get(url).text
+
+ def handleMatch(self, m):
+ gist_id = m.group('gist_id')
+ gist_file = m.group('filename')
+
+ gist_elem = etree.Element('div')
+ gist_elem.set('class', 'gist')
+ script_elem = etree.SubElement(gist_elem, 'script')
+
+ if gist_file:
+ script_elem.set('src', GIST_FILE_JS_URL.format(
+ gist_id, gist_file))
+
+ else:
+ script_elem.set('src', GIST_JS_URL.format(
+ gist_id))
+
+ if requests:
+ if gist_file:
+ raw_gist = (self.get_raw_gist_with_filename(
+ gist_id, gist_file))
+ script_elem.set('src', GIST_FILE_JS_URL.format(
+ gist_id, gist_file))
+
+ else:
+ raw_gist = (self.get_raw_gist(gist_id))
+ script_elem.set('src', GIST_JS_URL.format(
+ gist_id))
+
+ # Insert source as <pre/> within <noscript>
+ noscript_elem = etree.SubElement(gist_elem, 'noscript')
+ pre_elem = etree.SubElement(noscript_elem, 'pre')
+ pre_elem.text = AtomicString(raw_gist)
+
+ else:
+ warnings.warn('"requests" package not installed. '
+ 'Please install to add inline gist source.')
+
+ return gist_elem
+
+
+class GistExtension(Extension):
+ def __init__(self, configs={}):
+ # set extension defaults
+ self.config = {}
+
+ # Override defaults with user settings
+ for key, value in configs:
+ self.setConfig(key, value)
+
+ def extendMarkdown(self, md, md_globals):
+ gist_md_pattern = GistPattern(GIST_MD_RE, self.getConfigs())
+ gist_md_pattern.md = md
+ md.inlinePatterns.add('gist', gist_md_pattern, "<not_strong")
+
+ gist_rst_pattern = GistPattern(GIST_RST_RE, self.getConfigs())
+ gist_rst_pattern.md = md
+ md.inlinePatterns.add('gist-rst', gist_rst_pattern, ">gist")
+
+ md.registerExtension(self)
+
+
+def makeExtension(configs=None):
+ return GistExtension(configs)
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod(optionflags=(doctest.NORMALIZE_WHITESPACE +
+ doctest.REPORT_NDIFF))
diff --git a/nikola/plugins/compile_markdown/mdx_nikola.py b/nikola/plugins/compile_markdown/mdx_nikola.py
new file mode 100644
index 0000000..f7a1959
--- /dev/null
+++ b/nikola/plugins/compile_markdown/mdx_nikola.py
@@ -0,0 +1,56 @@
+# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Markdown Extension for Nikola-specific post-processing"""
+from __future__ import unicode_literals
+import re
+from markdown.postprocessors import Postprocessor
+from markdown.extensions import Extension
+
+
+class NikolaPostProcessor(Postprocessor):
+ def run(self, text):
+ output = text
+ # h1 is reserved for the title so increment all header levels
+ for n in reversed(range(1, 9)):
+ output = re.sub('<h%i>' % n, '<h%i>' % (n + 1), output)
+ output = re.sub('</h%i>' % n, '</h%i>' % (n + 1), output)
+
+ # python-markdown's highlighter uses the class 'codehilite' to wrap
+ # code, instead of the standard 'code'. None of the standard
+ # pygments stylesheets use this class, so swap it to be 'code'
+ output = re.sub(r'(<div[^>]+class="[^"]*)codehilite([^>]+)',
+ r'\1code\2', output)
+ return output
+
+
+class NikolaExtension(Extension):
+ def extendMarkdown(self, md, md_globals):
+ pp = NikolaPostProcessor()
+ md.postprocessors.add('nikola_post_processor', pp, '_end')
+ md.registerExtension(self)
+
+
+def makeExtension(configs=None):
+ return NikolaExtension(configs)
diff --git a/nikola/plugins/compile_markdown/mdx_podcast.py b/nikola/plugins/compile_markdown/mdx_podcast.py
new file mode 100644
index 0000000..be8bb6b
--- /dev/null
+++ b/nikola/plugins/compile_markdown/mdx_podcast.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2013 Michael Rabbitt, Roberto Alsina
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+# Inspired by "[Python] reStructuredText GitHub Podcast directive"
+# (https://gist.github.com/brianhsu/1407759), public domain by Brian Hsu
+
+from __future__ import print_function, unicode_literals
+
+
+'''
+Extension to Python Markdown for Embedded Audio
+
+Basic Example:
+
+>>> import markdown
+>>> text = """[podcast]http://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3[/podcast]"""
+>>> html = markdown.markdown(text, [PodcastExtension()])
+>>> print(html)
+<p><audio src="http://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3"></audio></p>
+'''
+
+from markdown.extensions import Extension
+from markdown.inlinepatterns import Pattern
+from markdown.util import etree
+
+PODCAST_RE = r'\[podcast\](?P<url>.+)\[/podcast\]'
+
+
+class PodcastPattern(Pattern):
+ """ InlinePattern for footnote markers in a document's body text. """
+
+ def __init__(self, pattern, configs):
+ Pattern.__init__(self, pattern)
+
+ def handleMatch(self, m):
+ url = m.group('url').strip()
+ audio_elem = etree.Element('audio')
+ audio_elem.set('controls', '')
+ source_elem = etree.SubElement(audio_elem, 'source')
+ source_elem.set('src', url)
+ source_elem.set('type', 'audio/mpeg')
+ return audio_elem
+
+
+class PodcastExtension(Extension):
+ def __init__(self, configs={}):
+ # set extension defaults
+ self.config = {}
+
+ # Override defaults with user settings
+ for key, value in configs:
+ self.setConfig(key, value)
+
+ def extendMarkdown(self, md, md_globals):
+ podcast_md_pattern = PodcastPattern(PODCAST_RE, self.getConfigs())
+ podcast_md_pattern.md = md
+ md.inlinePatterns.add('podcast', podcast_md_pattern, "<not_strong")
+ md.registerExtension(self)
+
+
+def makeExtension(configs=None):
+ return PodcastExtension(configs)
+
+if __name__ == '__main__':
+ import doctest
+ doctest.testmod(optionflags=(doctest.NORMALIZE_WHITESPACE +
+ doctest.REPORT_NDIFF))
diff --git a/nikola/plugins/compile_misaka.plugin b/nikola/plugins/compile_misaka.plugin
new file mode 100644
index 0000000..1b9c8a8
--- /dev/null
+++ b/nikola/plugins/compile_misaka.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = misaka
+Module = compile_misaka
+
+[Documentation]
+Author = Chris Lee
+Version = 0.1
+Website = http://c133.org/
+Description = Compile Markdown into HTML with Mikasa instead of python-markdown
+
diff --git a/nikola/plugins/compile_misaka/__init__.py b/nikola/plugins/compile_misaka/__init__.py
new file mode 100644
index 0000000..a3f687e
--- /dev/null
+++ b/nikola/plugins/compile_misaka/__init__.py
@@ -0,0 +1,82 @@
+# Copyright (c) 2013 Chris Lee
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""Implementation of compile_html based on misaka."""
+
+from __future__ import unicode_literals
+
+import codecs
+import os
+
+try:
+ import misaka
+
+except ImportError:
+ misaka = None # NOQA
+ nikola_extension = None
+ gist_extension = None
+ podcast_extension = None
+
+from nikola.plugin_categories import PageCompiler
+
+
+class CompileMarkdown(PageCompiler):
+ """Compile markdown into HTML."""
+
+ name = "markdown"
+
+ def __init__(self, *args, **kwargs):
+ super(CompileMarkdown, self).__init__(*args, **kwargs)
+ if misaka is not None:
+ self.ext = misaka.EXT_FENCED_CODE | misaka.EXT_STRIKETHROUGH | \
+ misaka.EXT_AUTOLINK | misaka.EXT_NO_INTRA_EMPHASIS
+
+ def compile_html(self, source, dest):
+ if misaka is None:
+ raise Exception('To build this site, you need to install the '
+ '"misaka" package.')
+ try:
+ os.makedirs(os.path.dirname(dest))
+ except:
+ pass
+ with codecs.open(dest, "w+", "utf8") as out_file:
+ with codecs.open(source, "r", "utf8") as in_file:
+ data = in_file.read()
+ output = misaka.html(data, extensions=self.ext)
+ out_file.write(output)
+
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+ d_name = os.path.dirname(path)
+ if not os.path.isdir(d_name):
+ os.makedirs(os.path.dirname(path))
+ with codecs.open(path, "wb+", "utf8") as fd:
+ if onefile:
+ fd.write('<!-- \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
+ fd.write('-->\n\n')
+ fd.write("\nWrite your post here.")
diff --git a/nikola/plugins/compile_rest/__init__.py b/nikola/plugins/compile_rest/__init__.py
index b0a0c00..3d41571 100644
--- a/nikola/plugins/compile_rest/__init__.py
+++ b/nikola/plugins/compile_rest/__init__.py
@@ -26,26 +26,28 @@ from __future__ import unicode_literals
import codecs
import os
-import docutils.core
-import docutils.io
-from docutils.parsers.rst import directives
-
-from .pygments_code_block_directive import (
- code_block_directive,
- listings_directive)
-directives.register_directive('code-block', code_block_directive)
-directives.register_directive('listing', listings_directive)
-
-from .youtube import youtube
-directives.register_directive('youtube', youtube)
-from .vimeo import vimeo
-directives.register_directive('vimeo', vimeo)
-from .slides import slides
-directives.register_directive('slides', slides)
-from .gist_directive import GitHubGist
-directives.register_directive('gist', GitHubGist)
-from .soundcloud import soundcloud
-directives.register_directive('soundcloud', soundcloud)
+try:
+ import docutils.core
+ import docutils.io
+ from docutils.parsers.rst import directives
+
+ from .listing import Listing, CodeBlock
+ directives.register_directive('code-block', CodeBlock)
+ directives.register_directive('sourcecode', CodeBlock)
+ directives.register_directive('listing', Listing)
+ from .youtube import Youtube
+ directives.register_directive('youtube', Youtube)
+ from .vimeo import Vimeo
+ directives.register_directive('vimeo', Vimeo)
+ from .slides import Slides
+ directives.register_directive('slides', Slides)
+ from .gist_directive import GitHubGist
+ directives.register_directive('gist', GitHubGist)
+ from .soundcloud import SoundCloud
+ directives.register_directive('soundcloud', SoundCloud)
+ has_docutils = True
+except ImportError:
+ has_docutils = False
from nikola.plugin_categories import PageCompiler
@@ -57,6 +59,9 @@ class CompileRest(PageCompiler):
def compile_html(self, source, dest):
"""Compile reSt into HTML."""
+ if not has_docutils:
+ raise Exception('To build this site, you need to install the '
+ '"docutils" package.')
try:
os.makedirs(os.path.dirname(dest))
except:
@@ -65,24 +70,38 @@ class CompileRest(PageCompiler):
with codecs.open(dest, "w+", "utf8") as out_file:
with codecs.open(source, "r", "utf8") as in_file:
data = in_file.read()
- output, error_level = rst2html(
- data, settings_overrides={'initial_header_level': 2})
+ output, error_level, deps = rst2html(
+ data, settings_overrides={
+ 'initial_header_level': 2,
+ 'record_dependencies': True,
+ 'stylesheet_path': None,
+ 'link_stylesheet': True,
+ 'syntax_highlight': 'short',
+ })
out_file.write(output)
+ deps_path = dest + '.dep'
+ if deps.list:
+ with codecs.open(deps_path, "wb+", "utf8") as deps_file:
+ deps_file.write('\n'.join(deps.list))
+ else:
+ if os.path.isfile(deps_path):
+ os.unlink(deps_path)
if error_level < 3:
return True
else:
return False
- def create_post(self, path, onefile=False, title="", slug="", date="",
- tags=""):
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+ d_name = os.path.dirname(path)
+ if not os.path.isdir(d_name):
+ os.makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
- fd.write('.. title: {0}\n'.format(title))
- fd.write('.. slug: {0}\n'.format(slug))
- fd.write('.. date: {0}\n'.format(date))
- fd.write('.. tags: {0}\n'.format(tags))
- fd.write('.. link: \n')
- fd.write('.. description: \n\n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
fd.write("\nWrite your post here.")
@@ -116,4 +135,4 @@ def rst2html(source, source_path=None, source_class=docutils.io.StringInput,
settings_overrides=settings_overrides,
config_section=config_section,
enable_exit_status=enable_exit_status)
- return pub.writer.parts['fragment'], pub.document.reporter.max_level
+ return pub.writer.parts['fragment'], pub.document.reporter.max_level, pub.settings.record_dependencies
diff --git a/nikola/plugins/compile_rest/dummy.py b/nikola/plugins/compile_rest/dummy.py
new file mode 100644
index 0000000..39543fd
--- /dev/null
+++ b/nikola/plugins/compile_rest/dummy.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"""A stupid codeblock replacement for neanderthals and users of Debian Sid."""
+
+from __future__ import unicode_literals
+
+from docutils import nodes
+from docutils.parsers.rst import Directive, directives
+
+CODE = '<pre>{0}</pre>'
+
+
+class CodeBlock(Directive):
+ required_arguments = 1
+ has_content = True
+
+ def run(self):
+ """ Required by the Directive interface. Create docutils nodes """
+ return [nodes.raw('', CODE.format('\n'.join(self.content)), format='html')]
+
+directives.register_directive('code', CodeBlock)
diff --git a/nikola/plugins/compile_rest/gist_directive.py b/nikola/plugins/compile_rest/gist_directive.py
index 0ea8f23..1506519 100644
--- a/nikola/plugins/compile_rest/gist_directive.py
+++ b/nikola/plugins/compile_rest/gist_directive.py
@@ -28,7 +28,7 @@ class GitHubGist(Directive):
return requests.get(url).text
def get_raw_gist(self, gistID):
- url = "https://raw.github.com/gist/{0}/".format(gistID)
+ url = "https://raw.github.com/gist/{0}".format(gistID)
return requests.get(url).text
def run(self):
diff --git a/nikola/plugins/compile_rest/listing.py b/nikola/plugins/compile_rest/listing.py
new file mode 100644
index 0000000..1b816f5
--- /dev/null
+++ b/nikola/plugins/compile_rest/listing.py
@@ -0,0 +1,121 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+""" Define and register a listing directive using the existing CodeBlock """
+
+
+from __future__ import unicode_literals
+from codecs import open as codecs_open # for patching purposes
+try:
+ from urlparse import urlunsplit
+except ImportError:
+ from urllib.parse import urlunsplit # NOQA
+
+from docutils import core
+from docutils.parsers.rst import directives
+try:
+ from docutils.parsers.rst.directives.body import CodeBlock
+except ImportError: # docutils < 0.9 (Debian Sid For The Loss)
+ from dummy import CodeBlock # NOQA
+
+import os
+
+
+class Listing(CodeBlock):
+ """ listing directive: create a CodeBlock from file
+
+ Usage:
+
+ .. listing:: nikola.py python
+ :number-lines:
+
+ """
+ has_content = False
+ required_arguments = 1
+ optional_arguments = 1
+
+ option_spec = {
+ 'start-at': directives.unchanged,
+ 'end-at': directives.unchanged,
+ 'start-after': directives.unchanged,
+ 'end-before': directives.unchanged,
+ }
+
+ def run(self):
+ fname = self.arguments.pop(0)
+ with codecs_open(os.path.join('listings', fname), 'rb+', 'utf8') as fileobject:
+ self.content = fileobject.read().splitlines()
+ self.trim_content()
+ target = urlunsplit(("link", 'listing', fname, '', ''))
+ generated_nodes = (
+ [core.publish_doctree('`{0} <{1}>`_'.format(fname, target))[0]])
+ generated_nodes += self.get_code_from_file(fileobject)
+ return generated_nodes
+
+ def trim_content(self):
+ """Cut the contents based in options."""
+ start = 0
+ end = len(self.content)
+ if 'start-at' in self.options:
+ for start, l in enumerate(self.content):
+ if self.options['start-at'] in l:
+ break
+ else:
+ start = 0
+ elif 'start-before' in self.options:
+ for start, l in enumerate(self.content):
+ if self.options['start-before'] in l:
+ if start > 0:
+ start -= 1
+ break
+ else:
+ start = 0
+ if 'end-at' in self.options:
+ for end, l in enumerate(self.content):
+ if self.options['end-at'] in l:
+ break
+ else:
+ end = len(self.content)
+ elif 'end-before' in self.options:
+ for end, l in enumerate(self.content):
+ if self.options['end-before'] in l:
+ end -= 1
+ break
+ else:
+ end = len(self.content)
+
+ self.content = self.content[start:end]
+
+ def get_code_from_file(self, data):
+ """ Create CodeBlock nodes from file object content """
+ return super(Listing, self).run()
+
+ def assert_has_content(self):
+ """ Listing has no content, override check from superclass """
+ pass
+
+
+directives.register_directive('listing', Listing)
diff --git a/nikola/plugins/compile_rest/pygments_code_block_directive.py b/nikola/plugins/compile_rest/pygments_code_block_directive.py
deleted file mode 100644
index 79bada2..0000000
--- a/nikola/plugins/compile_rest/pygments_code_block_directive.py
+++ /dev/null
@@ -1,424 +0,0 @@
-# -*- coding: utf-8 -*-
-#$Date: 2012-02-28 21:07:21 -0300 (Tue, 28 Feb 2012) $
-#$Revision: 2443 $
-
-# :Author: a Pygments author|contributor; Felix Wiemann; Guenter Milde
-# :Date: $Date: 2012-02-28 21:07:21 -0300 (Tue, 28 Feb 2012) $
-# :Copyright: This module has been placed in the public domain.
-#
-# This is a merge of `Using Pygments in ReST documents`_ from the pygments_
-# documentation, and a `proof of concept`_ by Felix Wiemann.
-#
-# ========== ===========================================================
-# 2007-06-01 Removed redundancy from class values.
-# 2007-06-04 Merge of successive tokens of same type
-# (code taken from pygments.formatters.others).
-# 2007-06-05 Separate docutils formatter script
-# Use pygments' CSS class names (like the html formatter)
-# allowing the use of pygments-produced style sheets.
-# 2007-06-07 Merge in the formatting of the parsed tokens
-# (misnamed as docutils_formatter) as class DocutilsInterface
-# 2007-06-08 Failsave implementation (fallback to a standard literal block
-# if pygments not found)
-# ========== ===========================================================
-#
-# ::
-
-"""Define and register a code-block directive using pygments"""
-
-from __future__ import unicode_literals
-
-# Requirements
-# ------------
-# ::
-
-import codecs
-from copy import copy
-import os
-try:
- from urlparse import urlunsplit
-except ImportError:
- from urllib.parse import urlunsplit # NOQA
-
-from docutils import nodes, core
-from docutils.parsers.rst import directives
-
-pygments = None
-try:
- import pygments
- from pygments.lexers import get_lexer_by_name
- from pygments.formatters.html import _get_ttype_class
-except ImportError:
- pass
-
-
-# Customisation
-# -------------
-#
-# Do not insert inline nodes for the following tokens.
-# (You could add e.g. Token.Punctuation like ``['', 'p']``.) ::
-
-unstyled_tokens = ['']
-
-
-# DocutilsInterface
-# -----------------
-#
-# This interface class combines code from
-# pygments.formatters.html and pygments.formatters.others.
-#
-# It does not require anything of docutils and could also become a part of
-# pygments::
-
-class DocutilsInterface(object):
- """Parse `code` string and yield "classified" tokens.
-
- Arguments
-
- code -- string of source code to parse
- language -- formal language the code is written in.
-
- Merge subsequent tokens of the same token-type.
-
- Yields the tokens as ``(ttype_class, value)`` tuples,
- where ttype_class is taken from pygments.token.STANDARD_TYPES and
- corresponds to the class argument used in pygments html output.
-
- """
-
- def __init__(self, code, language, custom_args={}):
- self.code = code
- self.language = language
- self.custom_args = custom_args
-
- def lex(self):
- """Get lexer for language (use text as fallback)"""
- try:
- if self.language and str(self.language).lower() != 'none':
- lexer = get_lexer_by_name(self.language.lower(),
- **self.custom_args)
- else:
- lexer = get_lexer_by_name('text', **self.custom_args)
- except ValueError:
- # what happens if pygment isn't present ?
- lexer = get_lexer_by_name('text')
- return pygments.lex(self.code, lexer)
-
- def join(self, tokens):
- """join subsequent tokens of same token-type
- """
- tokens = iter(tokens)
- (lasttype, lastval) = next(tokens)
- for ttype, value in tokens:
- if ttype is lasttype:
- lastval += value
- else:
- yield(lasttype, lastval)
- (lasttype, lastval) = (ttype, value)
- yield(lasttype, lastval)
-
- def __iter__(self):
- """parse code string and yield "clasified" tokens
- """
- try:
- tokens = self.lex()
- except IOError:
- yield ('', self.code)
- return
-
- for ttype, value in self.join(tokens):
- yield (_get_ttype_class(ttype), value)
-
-
-# code_block_directive
-# --------------------
-# ::
-
-def code_block_directive(name, arguments, options, content, lineno,
- content_offset, block_text, state, state_machine):
- """Parse and classify content of a code_block."""
- if 'include' in options:
- try:
- if 'encoding' in options:
- encoding = options['encoding']
- else:
- encoding = 'utf-8'
- content = codecs.open(
- options['include'], 'r', encoding).read().rstrip()
- except (IOError, UnicodeError): # no file or problem reading it
- content = ''
- line_offset = 0
- if content:
- # here we define the start-at and end-at options
- # so that limit is included in extraction
- # this is different than the start-after directive of docutils
- # (docutils/parsers/rst/directives/misc.py L73+)
- # which excludes the beginning
- # the reason is we want to be able to define a start-at like
- # def mymethod(self)
- # and have such a definition included
-
- after_text = options.get('start-at', None)
- if after_text:
- # skip content in include_text before
- # *and NOT incl.* a matching text
- after_index = content.find(after_text)
- if after_index < 0:
- raise state_machine.reporter.severe(
- 'Problem with "start-at" option of "{0}" '
- 'code-block directive:\nText not found.'.format(
- options['start-at']))
- # patch mmueller start
- # Move the after_index to the beginning of the line with the
- # match.
- for char in content[after_index:0:-1]:
- # codecs always opens binary. This works with '\n',
- # '\r' and '\r\n'. We are going backwards, so
- # '\n' is found first in '\r\n'.
- # Going with .splitlines() seems more appropriate
- # but needs a few more changes.
- if char == '\n' or char == '\r':
- break
- after_index -= 1
- # patch mmueller end
-
- content = content[after_index:]
- line_offset = len(content[:after_index].splitlines())
-
- after_text = options.get('start-after', None)
- if after_text:
- # skip content in include_text before
- # *and incl.* a matching text
- after_index = content.find(after_text)
- if after_index < 0:
- raise state_machine.reporter.severe(
- 'Problem with "start-after" option of "{0}" '
- 'code-block directive:\nText not found.'.format(
- options['start-after']))
- line_offset = len(content[:after_index +
- len(after_text)].splitlines())
- content = content[after_index + len(after_text):]
-
- # same changes here for the same reason
- before_text = options.get('end-at', None)
- if before_text:
- # skip content in include_text after
- # *and incl.* a matching text
- before_index = content.find(before_text)
- if before_index < 0:
- raise state_machine.reporter.severe(
- 'Problem with "end-at" option of "{0}" '
- 'code-block directive:\nText not found.'.format(
- options['end-at']))
- content = content[:before_index + len(before_text)]
-
- before_text = options.get('end-before', None)
- if before_text:
- # skip content in include_text after
- # *and NOT incl.* a matching text
- before_index = content.find(before_text)
- if before_index < 0:
- raise state_machine.reporter.severe(
- 'Problem with "end-before" option of "{0}" '
- 'code-block directive:\nText not found.'.format(
- options['end-before']))
- content = content[:before_index]
-
- else:
- content = '\n'.join(content)
-
- if 'tabsize' in options:
- tabw = options['tabsize']
- else:
- tabw = int(options.get('tab-width', 8))
-
- content = content.replace('\t', ' ' * tabw)
-
- withln = "linenos" in options
- if not "linenos_offset" in options:
- line_offset = 0
-
- language = arguments[0]
- # create a literal block element and set class argument
- code_block = nodes.literal_block(classes=["code", language])
-
- if withln:
- lineno = 1 + line_offset
- total_lines = content.count('\n') + 1 + line_offset
- lnwidth = len(str(total_lines))
- fstr = "\n%{0}d ".format(lnwidth)
- code_block += nodes.inline(fstr[1:].format(lineno),
- fstr[1:].format(lineno),
- classes=['linenumber'])
-
- # parse content with pygments and add to code_block element
- content = content.rstrip()
- if pygments is None:
- code_block += nodes.Text(content, content)
- else:
- # The [:-1] is because pygments adds a trailing \n which looks bad
- l = list(DocutilsInterface(content, language, options))
- if l[-1] == ('', '\n'):
- l = l[:-1]
- # We strip last element for the same reason (trailing \n looks bad)
- if l:
- l[-1] = (l[-1][0], l[-1][1].rstrip())
- for cls, value in l:
- if withln and "\n" in value:
- # Split on the "\n"s
- values = value.split("\n")
- # The first piece, pass as-is
- code_block += nodes.Text(values[0], values[0])
- # On the second and later pieces, insert \n and linenos
- linenos = list(range(lineno, lineno + len(values)))
- for chunk, ln in zip(values, linenos)[1:]:
- if ln <= total_lines:
- code_block += nodes.inline(fstr.format(ln),
- fstr.format(ln),
- classes=['linenumber'])
- code_block += nodes.Text(chunk, chunk)
- lineno += len(values) - 1
-
- elif cls in unstyled_tokens:
- # insert as Text to decrease the verbosity of the output.
- code_block += nodes.Text(value, value)
- else:
- code_block += nodes.inline(value, value, classes=[cls])
-
- return [code_block]
-
-# Custom argument validators
-# --------------------------
-# ::
-#
-# Move to separated module??
-
-
-def string_list(argument):
- """
- Converts a space- or comma-separated list of values into a python list
- of strings.
- (Directive option conversion function)
- Based in positive_int_list of docutils.parsers.rst.directives
- """
- if ',' in argument:
- entries = argument.split(',')
- else:
- entries = argument.split()
- return entries
-
-
-def string_bool(argument):
- """
- Converts True, true, False, False in python boolean values
- """
- if argument is None:
- msg = 'argument required but none supplied; choose "True" or "False"'
- raise ValueError(msg)
-
- elif argument.lower() == 'true':
- return True
- elif argument.lower() == 'false':
- return False
- else:
- raise ValueError('"{0}" unknown; choose from "True" or "False"'.format(
- argument))
-
-
-def csharp_unicodelevel(argument):
- return directives.choice(argument, ('none', 'basic', 'full'))
-
-
-def lhs_litstyle(argument):
- return directives.choice(argument, ('bird', 'latex'))
-
-
-def raw_compress(argument):
- return directives.choice(argument, ('gz', 'bz2'))
-
-
-def listings_directive(name, arguments, options, content, lineno,
- content_offset, block_text, state, state_machine):
- fname = arguments[0]
- options['include'] = os.path.join('listings', fname)
- target = urlunsplit(("link", 'listing', fname, '', ''))
- generated_nodes = [core.publish_doctree('`{0} <{1}>`_'.format(fname,
- target))[0]]
- generated_nodes += code_block_directive(name, [arguments[1]], options,
- content, lineno, content_offset,
- block_text, state, state_machine)
- return generated_nodes
-
-code_block_directive.arguments = (1, 0, 1)
-listings_directive.arguments = (2, 0, 1)
-code_block_directive.content = 1
-listings_directive.content = 1
-code_block_directive.options = {'include': directives.unchanged_required,
- 'start-at': directives.unchanged_required,
- 'end-at': directives.unchanged_required,
- 'start-after': directives.unchanged_required,
- 'end-before': directives.unchanged_required,
- 'linenos': directives.unchanged,
- 'linenos_offset': directives.unchanged,
- 'tab-width': directives.unchanged,
- # generic
- 'stripnl': string_bool,
- 'stripall': string_bool,
- 'ensurenl': string_bool,
- 'tabsize': directives.positive_int,
- 'encoding': directives.encoding,
- # Lua
- 'func_name_hightlighting': string_bool,
- 'disabled_modules': string_list,
- # Python Console
- 'python3': string_bool,
- # Delphi
- 'turbopascal': string_bool,
- 'delphi': string_bool,
- 'freepascal': string_bool,
- 'units': string_list,
- # Modula2
- 'pim': string_bool,
- 'iso': string_bool,
- 'objm2': string_bool,
- 'gm2ext': string_bool,
- # CSharp
- 'unicodelevel': csharp_unicodelevel,
- # Literate haskell
- 'litstyle': lhs_litstyle,
- # Raw
- 'compress': raw_compress,
- # Rst
- 'handlecodeblocks': string_bool,
- # Php
- 'startinline': string_bool,
- 'funcnamehighlighting': string_bool,
- 'disabledmodules': string_list,
- }
-
-listings_directive.options = copy(code_block_directive.options)
-listings_directive.options.pop('include')
-
-# .. _doctutils: http://docutils.sf.net/
-# .. _pygments: http://pygments.org/
-# .. _Using Pygments in ReST documents: http://pygments.org/docs/rstdirective/
-# .. _proof of concept:
-# http://article.gmane.org/gmane.text.docutils.user/3689
-#
-# Test output
-# -----------
-#
-# If called from the command line, call the docutils publisher to render the
-# input::
-
-if __name__ == '__main__':
- from docutils.core import publish_cmdline, default_description
- from docutils.parsers.rst import directives
- directives.register_directive('code-block', code_block_directive)
- description = "code-block directive test output" + default_description
- try:
- import locale
- locale.setlocale(locale.LC_ALL, '')
- except Exception:
- pass
- publish_cmdline(writer_name='html', description=description)
diff --git a/nikola/plugins/compile_rest/slides.py b/nikola/plugins/compile_rest/slides.py
index f9901f5..57fb754 100644
--- a/nikola/plugins/compile_rest/slides.py
+++ b/nikola/plugins/compile_rest/slides.py
@@ -22,71 +22,44 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-import json
+from __future__ import unicode_literals
from docutils import nodes
from docutils.parsers.rst import Directive, directives
-class slides(Directive):
+class Slides(Directive):
""" Restructured text extension for inserting slideshows."""
has_content = True
- option_spec = {
- "preload": directives.flag,
- "preloadImage": directives.uri,
- "container": directives.unchanged,
- "generateNextPrev": directives.flag,
- "next": directives.unchanged,
- "prev": directives.unchanged,
- "pagination": directives.flag,
- "generatePagination": directives.flag,
- "paginationClass": directives.unchanged,
- "currentClass": directives.unchanged,
- "fadeSpeed": directives.positive_int,
- "fadeEasing": directives.unchanged,
- "slideSpeed": directives.positive_int,
- "slideEasing": directives.unchanged,
- "start": directives.positive_int,
- "effect": directives.unchanged,
- "crossfade": directives.flag,
- "randomize": directives.flag,
- "play": directives.positive_int,
- "pause": directives.positive_int,
- "hoverPause": directives.flag,
- "autoHeight": directives.flag,
- "autoHeightSpeed": directives.positive_int,
- "bigTarget": directives.flag,
- "animationStart": directives.unchanged,
- "animationComplete": directives.unchanged,
- }
def run(self):
if len(self.content) == 0:
return
- for opt in ("preload", "generateNextPrev", "pagination",
- "generatePagination", "crossfade", "randomize",
- "hoverPause", "autoHeight", "bigTarget"):
- if opt in self.options:
- self.options[opt] = True
- options = {
- "autoHeight": True,
- "bigTarget": True,
- "paginationClass": "pager",
- "currentClass": "slide-current"
- }
- options.update(self.options)
- options = json.dumps(options)
output = []
- output.append('<script> $(function(){ $("#slides").slides(' + options +
- '); });'
- '</script>')
- output.append('<div id="slides" class="slides"><div '
- 'class="slides_container">')
- for image in self.content:
- output.append("""<div><img src="{0}"></div>""".format(image))
- output.append("""</div></div>""")
-
+ output.append("""
+ <div id="myCarousel" class="carousel slide">
+ <ol class="carousel-indicators">
+ """)
+ for i in range(len(self.content)):
+ if i == 0:
+ classname = 'class="active"'
+ else:
+ classname = ''
+ output.append(' <li data-target="#myCarousel" data-slide-to="{0}" {1}></li>'.format(i, classname))
+ output.append("""</ol>
+ <div class="carousel-inner">
+ """)
+ for i, image in enumerate(self.content):
+ if i == 0:
+ classname = "item active"
+ else:
+ classname = "item"
+ output.append("""<div class="{0}"><img src="{1}" alt="" style="margin: 0 auto 0 auto;"></div>""".format(classname, image))
+ output.append("""</div>
+ <a class="left carousel-control" href="#myCarousel" data-slide="prev">&lsaquo;</a>
+ <a class="right carousel-control" href="#myCarousel" data-slide="next">&rsaquo;</a>
+ </div>""")
return [nodes.raw('', '\n'.join(output), format='html')]
-directives.register_directive('slides', slides)
+directives.register_directive('slides', Slides)
diff --git a/nikola/plugins/compile_rest/soundcloud.py b/nikola/plugins/compile_rest/soundcloud.py
index d47bebf..6bdd4d5 100644
--- a/nikola/plugins/compile_rest/soundcloud.py
+++ b/nikola/plugins/compile_rest/soundcloud.py
@@ -1,5 +1,9 @@
+# coding: utf8
+
+
from docutils import nodes
-from docutils.parsers.rst import directives
+from docutils.parsers.rst import Directive, directives
+
CODE = ("""<iframe width="{width}" height="{height}"
scrolling="no" frameborder="no"
@@ -8,25 +12,39 @@ src="https://w.soundcloud.com/player/?url=http://api.soundcloud.com/tracks/"""
</iframe>""")
-def soundcloud(name, args, options, content, lineno,
- contentOffset, blockText, state, stateMachine):
- """ Restructured text extension for inserting SoundCloud embedded music """
- string_vars = {
- 'sid': content[0],
- 'width': 600,
- 'height': 160,
- 'extra': ''
+class SoundCloud(Directive):
+ """ Restructured text extension for inserting SoundCloud embedded music
+
+ Usage:
+ .. soundcloud:: <sound id>
+ :height: 400
+ :width: 600
+
+ """
+ has_content = True
+ required_arguments = 1
+ option_spec = {
+ 'width': directives.positive_int,
+ 'height': directives.positive_int,
}
- extra_args = content[1:] # Because content[0] is ID
- extra_args = [ea.strip().split("=") for ea in extra_args] # key=value
- extra_args = [ea for ea in extra_args if len(ea) == 2] # drop bad lines
- extra_args = dict(extra_args)
- if 'width' in extra_args:
- string_vars['width'] = extra_args.pop('width')
- if 'height' in extra_args:
- string_vars['height'] = extra_args.pop('height')
-
- return [nodes.raw('', CODE.format(**string_vars), format='html')]
-
-soundcloud.content = True
-directives.register_directive('soundcloud', soundcloud)
+
+ def run(self):
+ """ Required by the Directive interface. Create docutils nodes """
+ self.check_content()
+ options = {
+ 'sid': self.arguments[0],
+ 'width': 600,
+ 'height': 160,
+ }
+ options.update(self.options)
+ return [nodes.raw('', CODE.format(**options), format='html')]
+
+ def check_content(self):
+ """ Emit a deprecation warning if there is content """
+ if self.content:
+ raise self.warning("This directive does not accept content. The "
+ "'key=value' format for options is deprecated, "
+ "use ':key: value' instead")
+
+
+directives.register_directive('soundcloud', SoundCloud)
diff --git a/nikola/plugins/compile_rest/vimeo.py b/nikola/plugins/compile_rest/vimeo.py
index 34f2a50..c1dc143 100644
--- a/nikola/plugins/compile_rest/vimeo.py
+++ b/nikola/plugins/compile_rest/vimeo.py
@@ -1,3 +1,4 @@
+# coding: utf8
# Copyright (c) 2012 Roberto Alsina y otros.
# Permission is hereby granted, free of charge, to any
@@ -22,8 +23,9 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
from docutils import nodes
-from docutils.parsers.rst import directives
+from docutils.parsers.rst import Directive, directives
try:
import requests
@@ -37,6 +39,7 @@ except ImportError:
except ImportError:
json = None
+
CODE = """<iframe src="http://player.vimeo.com/video/{vimeo_id}"
width="{width}" height="{height}"
frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen>
@@ -47,46 +50,69 @@ VIDEO_DEFAULT_HEIGHT = 500
VIDEO_DEFAULT_WIDTH = 281
-def vimeo(name, args, options, content, lineno, contentOffset, blockText,
- state, stateMachine):
- """ Restructured text extension for inserting vimeo embedded videos """
- if requests is None:
- raise Exception("To use the Vimeo directive you need to install the "
- "requests module.")
- if json is None:
- raise Exception("To use the Vimeo directive you need python 2.6 or to "
- "install the simplejson module.")
- if len(content) == 0:
- return
-
- string_vars = {'vimeo_id': content[0]}
- extra_args = content[1:] # Because content[0] is ID
- extra_args = [ea.strip().split("=") for ea in extra_args] # key=value
- extra_args = [ea for ea in extra_args if len(ea) == 2] # drop bad lines
- extra_args = dict(extra_args)
- if 'width' in extra_args:
- string_vars['width'] = extra_args.pop('width')
- if 'height' in extra_args:
- string_vars['height'] = extra_args.pop('height')
-
- # Only need to make a connection if width and height aren't provided
- if 'height' not in string_vars or 'width' not in string_vars:
- string_vars['height'] = VIDEO_DEFAULT_HEIGHT
- string_vars['width'] = VIDEO_DEFAULT_WIDTH
-
- if json: # we can attempt to retrieve video attributes from vimeo
- try:
- url = ('http://vimeo.com/api/v2/video/{vimeo_id}'
- '.json'.format(**string_vars))
- data = requests.get(url).text
- video_attributes = json.loads(data)
- string_vars['height'] = video_attributes['height']
- string_vars['width'] = video_attributes['width']
- except Exception:
- # fall back to the defaults
- pass
-
- return [nodes.raw('', CODE.format(**string_vars), format='html')]
-
-vimeo.content = True
-directives.register_directive('vimeo', vimeo)
+class Vimeo(Directive):
+ """ Restructured text extension for inserting vimeo embedded videos
+
+ Usage:
+ .. vimeo:: 20241459
+ :height: 400
+ :width: 600
+
+ """
+ has_content = True
+ required_arguments = 1
+ option_spec = {
+ "width": directives.positive_int,
+ "height": directives.positive_int,
+ }
+
+ # set to False for not querying the vimeo api for size
+ request_size = True
+
+ def run(self):
+ self.check_content()
+ options = {
+ 'vimeo_id': self.arguments[0],
+ 'width': VIDEO_DEFAULT_WIDTH,
+ 'height': VIDEO_DEFAULT_HEIGHT,
+ }
+ if self.request_size:
+ self.check_modules()
+ self.set_video_size()
+ options.update(self.options)
+ return [nodes.raw('', CODE.format(**options), format='html')]
+
+ def check_modules(self):
+ if requests is None:
+ raise Exception("To use the Vimeo directive you need to install "
+ "the requests module.")
+ if json is None:
+ raise Exception("To use the Vimeo directive you need python 2.6 "
+ "or to install the simplejson module.")
+
+ def set_video_size(self):
+ # Only need to make a connection if width and height aren't provided
+ if 'height' not in self.options or 'width' not in self.options:
+ self.options['height'] = VIDEO_DEFAULT_HEIGHT
+ self.options['width'] = VIDEO_DEFAULT_WIDTH
+
+ if json: # we can attempt to retrieve video attributes from vimeo
+ try:
+ url = ('http://vimeo.com/api/v2/video/{0}'
+ '.json'.format(self.arguments[0]))
+ data = requests.get(url).text
+ video_attributes = json.loads(data)[0]
+ self.options['height'] = video_attributes['height']
+ self.options['width'] = video_attributes['width']
+ except Exception:
+ # fall back to the defaults
+ pass
+
+ def check_content(self):
+ if self.content:
+ raise self.warning("This directive does not accept content. The "
+ "'key=value' format for options is deprecated, "
+ "use ':key: value' instead")
+
+
+directives.register_directive('vimeo', Vimeo)
diff --git a/nikola/plugins/compile_rest/youtube.py b/nikola/plugins/compile_rest/youtube.py
index 30ac000..767be32 100644
--- a/nikola/plugins/compile_rest/youtube.py
+++ b/nikola/plugins/compile_rest/youtube.py
@@ -23,7 +23,8 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from docutils import nodes
-from docutils.parsers.rst import directives
+from docutils.parsers.rst import Directive, directives
+
CODE = """\
<iframe width="{width}"
@@ -32,25 +33,37 @@ src="http://www.youtube.com/embed/{yid}?rel=0&amp;hd=1&amp;wmode=transparent"
></iframe>"""
-def youtube(name, args, options, content, lineno,
- contentOffset, blockText, state, stateMachine):
- """ Restructured text extension for inserting youtube embedded videos """
- if len(content) == 0:
- return
- string_vars = {
- 'yid': content[0],
- 'width': 425,
- 'height': 344,
- 'extra': ''
+class Youtube(Directive):
+ """ Restructured text extension for inserting youtube embedded videos
+
+ Usage:
+ .. youtube:: lyViVmaBQDg
+ :height: 400
+ :width: 600
+
+ """
+ has_content = True
+ required_arguments = 1
+ option_spec = {
+ "width": directives.positive_int,
+ "height": directives.positive_int,
}
- extra_args = content[1:] # Because content[0] is ID
- extra_args = [ea.strip().split("=") for ea in extra_args] # key=value
- extra_args = [ea for ea in extra_args if len(ea) == 2] # drop bad lines
- extra_args = dict(extra_args)
- if 'width' in extra_args:
- string_vars['width'] = extra_args.pop('width')
- if 'height' in extra_args:
- string_vars['height'] = extra_args.pop('height')
- return [nodes.raw('', CODE.format(**string_vars), format='html')]
-youtube.content = True
-directives.register_directive('youtube', youtube)
+
+ def run(self):
+ self.check_content()
+ options = {
+ 'yid': self.arguments[0],
+ 'width': 425,
+ 'height': 344,
+ }
+ options.update(self.options)
+ return [nodes.raw('', CODE.format(**options), format='html')]
+
+ def check_content(self):
+ if self.content:
+ raise self.warning("This directive does not accept content. The "
+ "'key=value' format for options is deprecated, "
+ "use ':key: value' instead")
+
+
+directives.register_directive('youtube', Youtube)
diff --git a/nikola/plugins/compile_textile.py b/nikola/plugins/compile_textile.py
index 3ca370d..85efd3f 100644
--- a/nikola/plugins/compile_textile.py
+++ b/nikola/plugins/compile_textile.py
@@ -54,19 +54,17 @@ class CompileTextile(PageCompiler):
output = textile(data, head_offset=1)
out_file.write(output)
- def create_post(self, path, onefile=False, title="", slug="", date="",
- tags=""):
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
d_name = os.path.dirname(path)
if not os.path.isdir(d_name):
os.makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write('<notextile> <!--\n')
- fd.write('.. title: {0}\n'.format(title))
- fd.write('.. slug: {0}\n'.format(slug))
- fd.write('.. date: {0}\n'.format(date))
- fd.write('.. tags: {0}\n'.format(tags))
- fd.write('.. link: \n')
- fd.write('.. description: \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
fd.write('--></notextile>\n\n')
fd.write("\nWrite your post here.")
diff --git a/nikola/plugins/compile_txt2tags.py b/nikola/plugins/compile_txt2tags.py
index 90372bd..001da6e 100644
--- a/nikola/plugins/compile_txt2tags.py
+++ b/nikola/plugins/compile_txt2tags.py
@@ -57,19 +57,17 @@ class CompileTextile(PageCompiler):
cmd = ["-t", "html", "--no-headers", "--outfile", dest, source]
txt2tags(cmd)
- def create_post(self, path, onefile=False, title="", slug="", date="",
- tags=""):
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
d_name = os.path.dirname(path)
if not os.path.isdir(d_name):
os.makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
if onefile:
fd.write("\n'''\n<!--\n")
- fd.write('.. title: {0}\n'.format(title))
- fd.write('.. slug: {0}\n'.format(slug))
- fd.write('.. date: {0}\n'.format(date))
- fd.write('.. tags: {0}\n'.format(tags))
- fd.write('.. link: \n')
- fd.write('.. description: \n')
+ for k, v in metadata.items():
+ fd.write('.. {0}: {1}\n'.format(k, v))
fd.write("-->\n'''\n")
fd.write("\nWrite your post here.")
diff --git a/nikola/plugins/compile_wiki.py b/nikola/plugins/compile_wiki.py
index 1215506..fb9e010 100644
--- a/nikola/plugins/compile_wiki.py
+++ b/nikola/plugins/compile_wiki.py
@@ -57,14 +57,16 @@ class CompileTextile(PageCompiler):
output = HtmlEmitter(document).emit()
out_file.write(output)
- def create_post(self, path, onefile=False, title="", slug="", date="",
- tags=""):
+ def create_post(self, path, onefile=False, **kw):
+ metadata = {}
+ metadata.update(self.default_metadata)
+ metadata.update(kw)
+ d_name = os.path.dirname(path)
+ if not os.path.isdir(d_name):
+ os.makedirs(os.path.dirname(path))
if onefile:
raise Exception('There are no comments in CreoleWiki markup, so '
'one-file format is not possible, use the -2 '
'option.')
- d_name = os.path.dirname(path)
- if not os.path.isdir(d_name):
- os.makedirs(os.path.dirname(path))
with codecs.open(path, "wb+", "utf8") as fd:
fd.write("Write your post here.")
diff --git a/nikola/plugins/task_archive.py b/nikola/plugins/task_archive.py
index f91a10e..a67826f 100644
--- a/nikola/plugins/task_archive.py
+++ b/nikola/plugins/task_archive.py
@@ -22,7 +22,9 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+import calendar
import os
+import sys
from nikola.plugin_categories import Task
from nikola.utils import config_changed
@@ -39,16 +41,51 @@ class Archive(Task):
"translations": self.site.config['TRANSLATIONS'],
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
+ "create_monthly_archive": self.site.config['CREATE_MONTHLY_ARCHIVE'],
}
self.site.scan_posts()
# TODO add next/prev links for years
- template_name = "list_post.tmpl"
- # TODO: posts_per_year is global, kill it
- for year, posts in list(self.site.posts_per_year.items()):
- for lang in kw["translations"]:
+ for lang in kw["translations"]:
+ for year, posts in self.site.posts_per_year.items():
+ output_name = os.path.join(
+ kw['output_folder'], self.site.path("archive", year, lang))
+ context = {}
+ context["lang"] = lang
+ context["title"] = kw["messages"][lang]["Posts for year %s"] % year
+ context["permalink"] = self.site.link("archive", year, lang)
+ if not kw["create_monthly_archive"]:
+ template_name = "list_post.tmpl"
+ post_list = [self.site.global_data[post] for post in posts]
+ post_list.sort(key=lambda a: a.date)
+ post_list.reverse()
+ context["posts"] = post_list
+ else: # Monthly archives, just list the months
+ months = set([m.split('/')[1] for m in self.site.posts_per_month.keys() if m.startswith(str(year))])
+ months = sorted(list(months))
+ template_name = "list.tmpl"
+ context["items"] = [[get_month_name(int(month), lang), month] for month in months]
+ post_list = []
+ task = self.site.generic_post_list_renderer(
+ lang,
+ [],
+ output_name,
+ template_name,
+ kw['filters'],
+ context,
+ )
+ task_cfg = {1: task['uptodate'][0].config, 2: kw}
+ task['uptodate'] = [config_changed(task_cfg)]
+ task['basename'] = self.name
+ yield task
+
+ if not kw["create_monthly_archive"]:
+ continue # Just to avoid nesting the other loop in this if
+ template_name = "list_post.tmpl"
+ for yearmonth, posts in self.site.posts_per_month.items():
output_name = os.path.join(
- kw['output_folder'], self.site.path("archive", year,
- lang)).encode('utf8')
+ kw['output_folder'], self.site.path("archive", yearmonth,
+ lang))
+ year, month = yearmonth.split('/')
post_list = [self.site.global_data[post] for post in posts]
post_list.sort(key=lambda a: a.date)
post_list.reverse()
@@ -56,8 +93,9 @@ class Archive(Task):
context["lang"] = lang
context["posts"] = post_list
context["permalink"] = self.site.link("archive", year, lang)
- context["title"] = kw["messages"][lang]["Posts for year %s"]\
- % year
+
+ context["title"] = kw["messages"][lang]["Posts for {month} {year}"].format(
+ year=year, month=get_month_name(int(month), lang))
task = self.site.generic_post_list_renderer(
lang,
post_list,
@@ -80,7 +118,7 @@ class Archive(Task):
context = {}
output_name = os.path.join(
kw['output_folder'], self.site.path("archive", None,
- lang)).encode('utf8')
+ lang))
context["title"] = kw["messages"][lang]["Archive"]
context["items"] = [(year, self.site.link("archive", year, lang))
for year in years]
@@ -97,3 +135,13 @@ class Archive(Task):
task['uptodate'] = [config_changed(task_cfg)]
task['basename'] = self.name
yield task
+
+
+def get_month_name(month_no, locale):
+ if sys.version_info[0] == 3: # Python 3
+ with calendar.different_locale((locale, "UTF-8")):
+ s = calendar.month_name[month_no]
+ else: # Python 2
+ with calendar.TimeEncoding((locale, "UTF-8")):
+ s = calendar.month_name[month_no]
+ return s
diff --git a/nikola/plugins/task_copy_assets.py b/nikola/plugins/task_copy_assets.py
index 39fef5a..06d17e7 100644
--- a/nikola/plugins/task_copy_assets.py
+++ b/nikola/plugins/task_copy_assets.py
@@ -22,6 +22,7 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+import codecs
import os
from nikola.plugin_categories import Task
@@ -44,15 +45,20 @@ class CopyAssets(Task):
"themes": self.site.THEMES,
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
+ "code_color_scheme": self.site.config['CODE_COLOR_SCHEME'],
}
flag = True
+ has_code_css = False
tasks = {}
+ code_css_path = os.path.join(kw['output_folder'], 'assets', 'css', 'code.css')
for theme_name in kw['themes']:
src = os.path.join(utils.get_theme_path(theme_name), 'assets')
dst = os.path.join(kw['output_folder'], 'assets')
for task in utils.copy_tree(src, dst):
if task['name'] in tasks:
continue
+ if task['targets'][0] == code_css_path:
+ has_code_css = True
tasks[task['name']] = task
task['uptodate'] = [utils.config_changed(kw)]
task['basename'] = self.name
@@ -66,3 +72,22 @@ class CopyAssets(Task):
'uptodate': [True],
'actions': [],
}
+
+ if not has_code_css: # Generate it
+
+ def create_code_css():
+ from pygments.formatters import get_formatter_by_name
+ formatter = get_formatter_by_name('html', style=kw["code_color_scheme"])
+ with codecs.open(code_css_path, 'wb+', 'utf8') as outf:
+ outf.write(formatter.get_style_defs('.code'))
+ outf.write("table.codetable { width: 100%;} td.linenos {text-align: right; width: 4em;}")
+
+ task = {
+ 'basename': self.name,
+ 'name': code_css_path,
+ 'targets': [code_css_path],
+ 'uptodate': [utils.config_changed(kw)],
+ 'actions': [(create_code_css, [])],
+ 'clean': True,
+ }
+ yield utils.apply_filters(task, kw['filters'])
diff --git a/nikola/plugins/task_create_bundles.py b/nikola/plugins/task_create_bundles.py
index ad670e1..84ac0ab 100644
--- a/nikola/plugins/task_create_bundles.py
+++ b/nikola/plugins/task_create_bundles.py
@@ -22,6 +22,8 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+from __future__ import unicode_literals
+
import os
try:
@@ -53,6 +55,7 @@ class BuildBundles(LateTask):
'theme_bundles': get_theme_bundles(self.site.THEMES),
'themes': self.site.THEMES,
'files_folders': self.site.config['FILES_FOLDERS'],
+ 'code_color_scheme': self.site.config['CODE_COLOR_SCHEME'],
}
def build_bundle(output, inputs):
@@ -76,7 +79,7 @@ class BuildBundles(LateTask):
for name, files in kw['theme_bundles'].items():
output_path = os.path.join(kw['output_folder'], name)
dname = os.path.dirname(name)
- file_dep = [get_asset_path(
+ file_dep = [utils.get_asset_path(
os.path.join(dname, fname), kw['themes'],
kw['files_folders'])
for fname in files
@@ -101,47 +104,6 @@ class BuildBundles(LateTask):
}
-def get_asset_path(path, themes, files_folders={'files': ''}):
- """Checks which theme provides the path with the given asset,
- and returns the "real" path to the asset, relative to the
- current directory.
-
- If the asset is not provided by a theme, then it will be checked for
- in the FILES_FOLDERS
-
- >>> get_asset_path('assets/css/rst.css', ['site', 'default'])
- 'nikola/data/themes/default/assets/css/rst.css'
-
- >>> get_asset_path('assets/css/theme.css', ['site', 'default'])
- 'nikola/data/themes/site/assets/css/theme.css'
-
- >>> get_asset_path('nikola.py', ['site', 'default'], {'nikola': ''})
- 'nikola/nikola.py'
-
- >>> get_asset_path('nikola/nikola.py', ['site', 'default'],
- ... {'nikola':'nikola'})
- 'nikola/nikola.py'
-
- """
- for theme_name in themes:
- candidate = os.path.join(
- utils.get_theme_path(theme_name),
- path
- )
- if os.path.isfile(candidate):
- return os.path.relpath(candidate, os.getcwd())
- for src, rel_dst in files_folders.items():
- candidate = os.path.join(
- src,
- os.path.relpath(path, rel_dst)
- )
- if os.path.isfile(candidate):
- return os.path.relpath(candidate, os.getcwd())
-
- # whatever!
- return None
-
-
def get_theme_bundles(themes):
"""Given a theme chain, return the bundle definitions."""
bundles = {}
diff --git a/nikola/plugins/task_indexes.py b/nikola/plugins/task_indexes.py
index 7baf660..aa5e648 100644
--- a/nikola/plugins/task_indexes.py
+++ b/nikola/plugins/task_indexes.py
@@ -46,31 +46,35 @@ class Indexes(Task):
"index_teasers": self.site.config['INDEX_TEASERS'],
"output_folder": self.site.config['OUTPUT_FOLDER'],
"filters": self.site.config['FILTERS'],
+ "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
+ "indexes_title": self.site.config['INDEXES_TITLE'],
+ "indexes_pages": self.site.config['INDEXES_PAGES'],
+ "blog_title": self.site.config["BLOG_TITLE"],
}
template_name = "index.tmpl"
- # TODO: timeline is global, get rid of it
posts = [x for x in self.site.timeline if x.use_in_feeds]
- # Split in smaller lists
- lists = []
- while posts:
- lists.append(posts[:kw["index_display_post_count"]])
- posts = posts[kw["index_display_post_count"]:]
- num_pages = len(lists)
- if not lists:
+ if not posts:
yield {'basename': 'render_indexes', 'actions': []}
for lang in kw["translations"]:
+ # Split in smaller lists
+ lists = []
+ if kw["hide_untranslated_posts"]:
+ filtered_posts = [x for x in posts if x.is_translation_available(lang)]
+ else:
+ filtered_posts = posts
+ while filtered_posts:
+ lists.append(filtered_posts[:kw["index_display_post_count"]])
+ filtered_posts = filtered_posts[kw["index_display_post_count"]:]
+ num_pages = len(lists)
for i, post_list in enumerate(lists):
context = {}
- if self.site.config.get("INDEXES_TITLE", ""):
- indexes_title = self.site.config['INDEXES_TITLE']
- else:
- indexes_title = self.site.config["BLOG_TITLE"]
+ indexes_title = kw['indexes_title'] or kw['blog_title']
if not i:
context["title"] = indexes_title
else:
- if self.site.config.get("INDEXES_PAGES", ""):
- indexes_pages = self.site.config["INDEXES_PAGES"] % i
+ if kw["indexes_pages"]:
+ indexes_pages = kw["indexes_pages"] % i
else:
indexes_pages = " (" + \
kw["messages"][lang]["old posts page %d"] % i + ")"
@@ -87,7 +91,7 @@ class Indexes(Task):
context["permalink"] = self.site.link("index", i, lang)
output_name = os.path.join(
kw['output_folder'], self.site.path("index", i,
- lang)).encode('utf8')
+ lang))
task = self.site.generic_post_list_renderer(
lang,
post_list,
@@ -103,7 +107,6 @@ class Indexes(Task):
if not self.site.config["STORY_INDEX"]:
return
- # TODO: do story indexes as described in #232
kw = {
"translations": self.site.config['TRANSLATIONS'],
"post_pages": self.site.config["post_pages"],
diff --git a/nikola/plugins/task_localsearch.plugin b/nikola/plugins/task_localsearch.plugin
new file mode 100644
index 0000000..33eb78b
--- /dev/null
+++ b/nikola/plugins/task_localsearch.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = local_search
+Module = task_localsearch
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Create data files for local search via Tipue
+
diff --git a/nikola/plugins/task_localsearch/MIT-LICENSE.txt b/nikola/plugins/task_localsearch/MIT-LICENSE.txt
new file mode 100644
index 0000000..f131068
--- /dev/null
+++ b/nikola/plugins/task_localsearch/MIT-LICENSE.txt
@@ -0,0 +1,20 @@
+Tipue Search Copyright (c) 2012 Tipue
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/nikola/plugins/task_localsearch/__init__.py b/nikola/plugins/task_localsearch/__init__.py
new file mode 100644
index 0000000..db8610a
--- /dev/null
+++ b/nikola/plugins/task_localsearch/__init__.py
@@ -0,0 +1,102 @@
+# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import unicode_literals
+import codecs
+import json
+import os
+
+from nikola.plugin_categories import LateTask
+from nikola.utils import config_changed, copy_tree
+
+# This is what we need to produce:
+#var tipuesearch = {"pages": [
+ #{"title": "Tipue Search, a jQuery site search engine", "text": "Tipue
+ #Search is a site search engine jQuery plugin. It's free for both commercial and
+ #non-commercial use and released under the MIT License. Tipue Search includes
+ #features such as word stemming and word replacement.", "tags": "JavaScript",
+ #"loc": "http://www.tipue.com/search"},
+ #{"title": "Tipue Search demo", "text": "Tipue Search demo. Tipue Search is
+ #a site search engine jQuery plugin.", "tags": "JavaScript", "loc":
+ #"http://www.tipue.com/search/demo"},
+ #{"title": "About Tipue", "text": "Tipue is a small web development/design
+ #studio based in North London. We've been around for over a decade.", "tags": "",
+ #"loc": "http://www.tipue.com/about"}
+#]};
+
+
+class Tipue(LateTask):
+ """Render the blog posts as JSON data."""
+
+ name = "local_search"
+
+ def gen_tasks(self):
+ self.site.scan_posts()
+
+ kw = {
+ "translations": self.site.config['TRANSLATIONS'],
+ "output_folder": self.site.config['OUTPUT_FOLDER'],
+ }
+
+ posts = self.site.timeline[:]
+ dst_path = os.path.join(kw["output_folder"], "assets", "js",
+ "tipuesearch_content.json")
+
+ def save_data():
+ pages = []
+ for lang in kw["translations"]:
+ for post in posts:
+ # Don't index drafts (Issue #387)
+ if post.is_draft:
+ continue
+ text = post.text(lang, strip_html=True)
+ text = text.replace('^', '')
+
+ data = {}
+ data["title"] = post.title(lang)
+ data["text"] = text
+ data["tags"] = ",".join(post.tags)
+ data["loc"] = post.permalink(lang)
+ pages.append(data)
+ output = json.dumps({"pages": pages}, indent=2)
+ try:
+ os.makedirs(os.path.dirname(dst_path))
+ except:
+ pass
+ with codecs.open(dst_path, "wb+", "utf8") as fd:
+ fd.write(output)
+
+ yield {
+ "basename": str(self.name),
+ "name": dst_path,
+ "targets": [dst_path],
+ "actions": [(save_data, [])],
+ 'uptodate': [config_changed(kw)]
+ }
+
+ # Copy all the assets to the right places
+ asset_folder = os.path.join(os.path.dirname(__file__), "files")
+ for task in copy_tree(asset_folder, kw["output_folder"]):
+ task["basename"] = str(self.name)
+ yield task
diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/expand.png b/nikola/plugins/task_localsearch/files/assets/css/img/expand.png
new file mode 100755
index 0000000..21bb7b0
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/assets/css/img/expand.png
Binary files differ
diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/link.png b/nikola/plugins/task_localsearch/files/assets/css/img/link.png
new file mode 100755
index 0000000..d4e51c5
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/assets/css/img/link.png
Binary files differ
diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/loader.gif b/nikola/plugins/task_localsearch/files/assets/css/img/loader.gif
new file mode 100644
index 0000000..9c97738
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/assets/css/img/loader.gif
Binary files differ
diff --git a/nikola/plugins/task_localsearch/files/assets/css/img/search.gif b/nikola/plugins/task_localsearch/files/assets/css/img/search.gif
new file mode 100644
index 0000000..644bd17
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/assets/css/img/search.gif
Binary files differ
diff --git a/nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css b/nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css
new file mode 100755
index 0000000..96dadf0
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/assets/css/tipuesearch.css
@@ -0,0 +1,232 @@
+
+/*
+Tipue Search 2.1
+Copyright (c) 2013 Tipue
+Tipue Search is released under the MIT License
+http://www.tipue.com/search
+*/
+
+
+em
+{
+ font: inherit;
+ font-weight: 400;
+}
+#tipue_search_input
+{
+}
+#tipue_search_input:focus
+{
+ border-color: #c3c3c3;
+ box-shadow: 0 0 3px rgba(0,0,0,.2);
+}
+#tipue_search_button
+{
+ width: 60px;
+ height: 33px;
+ margin-top: 1px;
+ border: 1px solid #dcdcdc;
+ border-radius: 2px;
+ background: #f1f1f1 url('img/search.gif') no-repeat center;
+ outline: none;
+}
+#tipue_search_button:hover
+{
+ border: 1px solid #c3c3c3;
+ -moz-box-shadow: 1px 1px 2px #e3e3e3;
+ -webkit-box-shadow: 1px 1px 2px #e3e3e3;
+ box-shadow: 1px 1px 2px #e3e3e3;
+}
+
+#tipue_search_content
+{
+ clear: left;
+ max-width: 650px;
+ padding: 25px 0 13px 0;
+ margin: 0;
+}
+#tipue_search_loading
+{
+ padding-top: 60px;
+ background: #fff url('img/loader.gif') no-repeat left;
+}
+
+#tipue_search_warning_head
+{
+ font: 14px/1.5 'open sans', sans-serif;
+ color: #333;
+}
+#tipue_search_warning
+{
+ font: 300 14px/1.5 lato, sans-serif;
+ color: #111;
+ margin: 13px 0;
+}
+#tipue_search_warning a
+{
+ color: #36c;
+ text-decoration: none;
+}
+#tipue_search_warning a:hover
+{
+ color: #111;
+}
+
+#tipue_search_results_count
+{
+ font: 300 14px/1.5 lato, sans-serif;
+ color: #111;
+}
+
+.tipue_search_content_title
+{
+ font: 300 19px/1.5 'open sans', sans-serif;
+ margin-top: 31px;
+}
+.tipue_search_content_title a
+{
+ color: #36c;
+ text-decoration: none;
+}
+.tipue_search_content_title a:hover
+{
+ color: #333;
+}
+
+.tipue_search_content_image_box
+{
+ float: left;
+ border: 1px solid #f3f3f3;
+ padding: 13px;
+ margin: 21px 0 7px 0;
+}
+.tipue_search_content_image
+{
+ max-width: 110px;
+ height: auto;
+ outline: none;
+ cursor: pointer;
+}
+#tipue_lightbox
+{
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(255, 255, 255, .9);
+}
+#tipue_lightbox_content
+{
+ margin: 37px auto;
+ background-color: #fff;
+ padding: 30px;
+ border: 1px solid #ccc;
+ width: 250px;
+ text-align: center;
+ box-shadow: 0 1px 2px #eee;
+}
+#tipue_lightbox img
+{
+ max-width: 200px;
+ height: auto;
+}
+#tipue_lightbox_content_title
+{
+ font: 300 14px/1.7 lato, sans-serif;
+ color: #111;
+ padding: 17px 25px 0 25px;
+ width: 200px;
+}
+#tipue_lightbox_content_link
+{
+ float: left;
+ width: 30px;
+ height: 30px;
+ margin-top: 17px;
+ background: #fff url('img/link.png') no-repeat center;
+}
+#tipue_lightbox_content_expand
+{
+ float: left;
+ width: 30px;
+ height: 30px;
+ margin: 17px 0 0 3px;
+ background: #fff url('img/expand.png') no-repeat center;
+}
+#tipue_lightbox_content_link:hover, #tipue_lightbox_content_expand:hover
+{
+ background-color: #f3f3f3;
+}
+
+.tipue_search_content_text
+{
+ font: 300 14px/1.7 lato, sans-serif;
+ color: #111;
+ padding: 13px 0;
+}
+.tipue_search_content_loc
+{
+ font: 300 14px/1.5 lato, sans-serif;
+ color: #111;
+}
+.tipue_search_content_loc a
+{
+ color: #555;
+ text-decoration: none;
+}
+.tipue_search_content_loc a:hover
+{
+ padding-bottom: 1px;
+ border-bottom: 1px solid #ccc;
+}
+
+#tipue_search_foot
+{
+ margin: 47px 0 31px 0;
+}
+#tipue_search_foot_boxes
+{
+ padding: 0;
+ margin: 0;
+ font: 12px/1 'open sans', sans-serif;
+}
+#tipue_search_foot_boxes li
+{
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: inline;
+}
+#tipue_search_foot_boxes li a
+{
+ padding: 7px 10px 8px 10px;
+ background-color: #f5f5f5;
+ background: -webkit-linear-gradient(top, #f7f7f7, #f1f1f1);
+ background: -moz-linear-gradient(top, #f7f7f7, #f1f1f1);
+ background: -ms-linear-gradient(top, #f7f7f7, #f1f1f1);
+ background: -o-linear-gradient(top, #f7f7f7, #f1f1f1);
+ background: linear-gradient(top, #f7f7f7, #f1f1f1);
+ border: 1px solid #dcdcdc;
+ border-radius: 2px;
+ color: #333;
+ margin-right: 7px;
+ text-decoration: none;
+ text-align: center;
+}
+#tipue_search_foot_boxes li.current
+{
+ padding: 7px 10px 8px 10px;
+ background: #fff;
+ border: 1px solid #dcdcdc;
+ border-radius: 2px;
+ color: #333;
+ margin-right: 7px;
+ text-align: center;
+}
+#tipue_search_foot_boxes li a:hover
+{
+ border: 1px solid #c3c3c3;
+ box-shadow: 1px 1px 2px #e3e3e3;
+}
diff --git a/nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js b/nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js
new file mode 100644
index 0000000..5c766ea
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/assets/js/tipuesearch.js
@@ -0,0 +1,426 @@
+
+/*
+Tipue Search 2.1
+Copyright (c) 2013 Tipue
+Tipue Search is released under the MIT License
+http://www.tipue.com/search
+*/
+
+
+(function($) {
+
+ $.fn.tipuesearch = function(options) {
+
+ var set = $.extend( {
+
+ 'show' : 7,
+ 'newWindow' : false,
+ 'showURL' : true,
+ 'minimumLength' : 3,
+ 'descriptiveWords' : 25,
+ 'highlightTerms' : true,
+ 'highlightEveryTerm' : false,
+ 'mode' : 'static',
+ 'liveDescription' : '*',
+ 'liveContent' : '*',
+ 'contentLocation' : 'tipuesearch/tipuesearch_content.json'
+
+ }, options);
+
+ return this.each(function() {
+
+ var tipuesearch_in = {
+ pages: []
+ };
+ $.ajaxSetup({
+ async: false
+ });
+
+ if (set.mode == 'live')
+ {
+ for (var i = 0; i < tipuesearch_pages.length; i++)
+ {
+ $.get(tipuesearch_pages[i], '',
+ function (html)
+ {
+ var cont = $(set.liveContent, html).text();
+ cont = cont.replace(/\s+/g, ' ');
+ var desc = $(set.liveDescription, html).text();
+ desc = desc.replace(/\s+/g, ' ');
+
+ var t_1 = html.toLowerCase().indexOf('<title>');
+ var t_2 = html.toLowerCase().indexOf('</title>', t_1 + 7);
+ if (t_1 != -1 && t_2 != -1)
+ {
+ var tit = html.slice(t_1 + 7, t_2);
+ }
+ else
+ {
+ var tit = 'No title';
+ }
+
+ tipuesearch_in.pages.push({
+ "title": tit,
+ "text": desc,
+ "tags": cont,
+ "loc": tipuesearch_pages[i]
+ });
+ }
+ );
+ }
+ }
+
+ if (set.mode == 'json')
+ {
+ $.getJSON(set.contentLocation,
+ function(json)
+ {
+ tipuesearch_in = $.extend({}, json);
+ }
+ );
+ }
+
+ if (set.mode == 'static' || set.mode == 'static-images')
+ {
+ tipuesearch_in = $.extend({}, tipuesearch);
+ }
+
+ var tipue_search_w = '';
+ if (set.newWindow)
+ {
+ tipue_search_w = ' target="_blank"';
+ }
+
+ function getURLP(name)
+ {
+ return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,""])[1].replace(/\+/g, '%20')) || null;
+ }
+ if (getURLP('q'))
+ {
+ $('#tipue_search_input').val(getURLP('q'));
+ getTipueSearch(0, true);
+ }
+
+ $('#tipue_search_button').click(function()
+ {
+ getTipueSearch(0, true);
+ });
+ $(this).keyup(function(event)
+ {
+ if(event.keyCode == '13')
+ {
+ getTipueSearch(0, true);
+ }
+ });
+
+ function getTipueSearch(start, replace)
+ {
+ $('#tipue_search_content').hide();
+ var out = '';
+ var results = '';
+ var show_replace = false;
+ var show_stop = false;
+
+ var d = $('#tipue_search_input').val().toLowerCase();
+ d = $.trim(d);
+ var d_w = d.split(' ');
+ d = '';
+ for (var i = 0; i < d_w.length; i++)
+ {
+ var a_w = true;
+ for (var f = 0; f < tipuesearch_stop_words.length; f++)
+ {
+ if (d_w[i] == tipuesearch_stop_words[f])
+ {
+ a_w = false;
+ show_stop = true;
+ }
+ }
+ if (a_w)
+ {
+ d = d + ' ' + d_w[i];
+ }
+ }
+ d = $.trim(d);
+ d_w = d.split(' ');
+
+ if (d.length >= set.minimumLength)
+ {
+ if (replace)
+ {
+ var d_r = d;
+ for (var i = 0; i < d_w.length; i++)
+ {
+ for (var f = 0; f < tipuesearch_replace.words.length; f++)
+ {
+ if (d_w[i] == tipuesearch_replace.words[f].word)
+ {
+ d = d.replace(d_w[i], tipuesearch_replace.words[f].replace_with);
+ show_replace = true;
+ }
+ }
+ }
+ d_w = d.split(' ');
+ }
+
+ var d_t = d;
+ for (var i = 0; i < d_w.length; i++)
+ {
+ for (var f = 0; f < tipuesearch_stem.words.length; f++)
+ {
+ if (d_w[i] == tipuesearch_stem.words[f].word)
+ {
+ d_t = d_t + ' ' + tipuesearch_stem.words[f].stem;
+ }
+ }
+ }
+ d_w = d_t.split(' ');
+
+ var c = 0;
+ found = new Array();
+ for (var i = 0; i < tipuesearch_in.pages.length; i++)
+ {
+ var score = 1000000000;
+ var s_t = tipuesearch_in.pages[i].text;
+ for (var f = 0; f < d_w.length; f++)
+ {
+ var pat = new RegExp(d_w[f], 'i');
+ if (tipuesearch_in.pages[i].title.search(pat) != -1)
+ {
+ score -= (200000 - i);
+ }
+ if (tipuesearch_in.pages[i].text.search(pat) != -1)
+ {
+ score -= (150000 - i);
+ }
+
+ if (set.highlightTerms)
+ {
+ if (set.highlightEveryTerm)
+ {
+ var patr = new RegExp('(' + d_w[f] + ')', 'gi');
+ }
+ else
+ {
+ var patr = new RegExp('(' + d_w[f] + ')', 'i');
+ }
+ s_t = s_t.replace(patr, "<em>$1</em>");
+ }
+
+ if (tipuesearch_in.pages[i].tags.search(pat) != -1)
+ {
+ score -= (100000 - i);
+ }
+ }
+ if (score < 1000000000)
+ {
+ if (set.mode == 'static-images')
+ {
+ found[c++] = score + '^' + tipuesearch_in.pages[i].title + '^' + s_t + '^' + tipuesearch_in.pages[i].loc + '^' + tipuesearch_in.pages[i].image;
+ }
+ else
+ {
+ found[c++] = score + '^' + tipuesearch_in.pages[i].title + '^' + s_t + '^' + tipuesearch_in.pages[i].loc;
+ }
+ }
+ }
+
+ if (c != 0)
+ {
+ if (show_replace == 1)
+ {
+ out += '<div id="tipue_search_warning_head">Showing results for ' + d + '</div>';
+ out += '<div id="tipue_search_warning">Show results for <a href="javascript:void(0)" id="tipue_search_replaced">' + d_r + '</a></div>';
+ }
+ if (c == 1)
+ {
+ out += '<div id="tipue_search_results_count">1 result</div>';
+ }
+ else
+ {
+ c_c = c.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+ out += '<div id="tipue_search_results_count">' + c_c + ' results</div>';
+ }
+
+ found.sort();
+ var l_o = 0;
+ for (var i = 0; i < found.length; i++)
+ {
+ var fo = found[i].split('^');
+ if (l_o >= start && l_o < set.show + start)
+ {
+ out += '<div class="tipue_search_content_title"><a href="' + fo[3] + '"' + tipue_search_w + '>' + fo[1] + '</a></div>';
+
+ if (set.mode == 'static-images')
+ {
+ if (fo[4])
+ {
+ out += '<div class="tipue_search_content_image_box"><img class="tipue_search_content_image" id="' + fo[1] + '^' + fo[3] + '" ';
+ out += 'src=' + fo[4] + '></div><div style="clear: both;"></div>';
+ }
+ }
+
+ var t = fo[2];
+ var t_d = '';
+ var t_w = t.split(' ');
+ if (t_w.length < set.descriptiveWords)
+ {
+ t_d = t;
+ }
+ else
+ {
+ for (var f = 0; f < set.descriptiveWords; f++)
+ {
+ t_d += t_w[f] + ' ';
+ }
+ }
+ t_d = $.trim(t_d);
+ if (t_d.charAt(t_d.length - 1) != '.')
+ {
+ t_d += ' ...';
+ }
+ out += '<div class="tipue_search_content_text">' + t_d + '</div>';
+
+ if (set.showURL)
+ {
+ out += '<div class="tipue_search_content_loc"><a href="' + fo[3] + '"' + tipue_search_w + '>' + fo[3] + '</a></div>';
+ }
+ }
+ l_o++;
+ }
+
+ if (c > set.show)
+ {
+ var pages = Math.ceil(c / set.show);
+ var page = (start / set.show);
+ out += '<div id="tipue_search_foot"><ul id="tipue_search_foot_boxes">';
+
+ if (start > 0)
+ {
+ out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start - set.show) + '_' + replace + '">&#171; Previous</a></li>';
+ }
+
+ if (page <= 4)
+ {
+ var p_b = pages;
+ if (pages > 5)
+ {
+ p_b = 5;
+ }
+ for (var f = 0; f < p_b; f++)
+ {
+ if (f == page)
+ {
+ out += '<li class="current">' + (f + 1) + '</li>';
+ }
+ else
+ {
+ out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (f * set.show) + '_' + replace + '">' + (f + 1) + '</a></li>';
+ }
+ }
+ }
+ else
+ {
+ var p_b = pages + 4;
+ if (p_b > pages)
+ {
+ p_b = pages;
+ }
+ for (var f = page; f < p_b; f++)
+ {
+ if (f == page)
+ {
+ out += '<li class="current">' + (f + 1) + '</li>';
+ }
+ else
+ {
+ out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (f * set.show) + '_' + replace + '">' + (f + 1) + '</a></li>';
+ }
+ }
+ }
+
+ if (page + 1 != pages)
+ {
+ out += '<li><a href="javascript:void(0)" class="tipue_search_foot_box" id="' + (start + set.show) + '_' + replace + '">Next &#187;</a></li>';
+ }
+
+ out += '</ul></div>';
+ }
+ }
+ else
+ {
+ out += '<div id="tipue_search_warning_head">Nothing found</div>';
+ }
+ }
+ else
+ {
+ if (show_stop)
+ {
+ out += '<div id="tipue_search_warning_head">Nothing found</div><div id="tipue_search_warning">Common words are largely ignored</div>';
+ }
+ else
+ {
+ out += '<div id="tipue_search_warning_head">Search too short</div>';
+ if (set.minimumLength == 1)
+ {
+ out += '<div id="tipue_search_warning">Should be one character or more</div>';
+ }
+ else
+ {
+ out += '<div id="tipue_search_warning">Should be ' + set.minimumLength + ' characters or more</div>';
+ }
+ }
+ }
+
+ $('#tipue_search_content').html(out);
+ $('#tipue_search_content').slideDown(200);
+
+ $('#tipue_search_replaced').click(function()
+ {
+ getTipueSearch(0, false);
+ });
+
+ $('.tipue_search_content_image').click(function()
+ {
+ var src_i = $(this).attr('src');
+ var id_v = $(this).attr('id');
+ var id_a = id_v.split('^');
+
+ var l_c_i = '<img src="' + src_i + '"><div id="tipue_lightbox_content_title">' + id_a[0] + '</div>';
+ l_c_i += '<a href="' + id_a[1] + '"' + tipue_search_w + '><div id="tipue_lightbox_content_link"></div></a>';
+ l_c_i += '<a href="' + src_i + '"' + tipue_search_w + '><div id="tipue_lightbox_content_expand"></div></a><div style="clear: both;"></div>';
+
+ if ($('#tipue_lightbox').length > 0)
+ {
+ $('#tipue_lightbox_content').html(l_c_i);
+ $('#tipue_lightbox').fadeIn();
+ }
+ else
+ {
+ var tipue_lightbox = '<div id="tipue_lightbox">' + '<div id="tipue_lightbox_content">' + l_c_i + '</div></div>';
+ $('body').append(tipue_lightbox);
+ $('#tipue_lightbox').fadeIn();
+ }
+ });
+ $('#tipue_lightbox').live('click', function()
+ {
+ $('#tipue_lightbox').hide();
+ });
+
+ $('.tipue_search_foot_box').click(function()
+ {
+ var id_v = $(this).attr('id');
+ var id_a = id_v.split('_');
+
+ getTipueSearch(parseInt(id_a[0]), id_a[1]);
+ });
+ }
+
+ });
+ };
+
+})(jQuery);
+
+
+
+
diff --git a/nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js b/nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js
new file mode 100644
index 0000000..8989c3c
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/assets/js/tipuesearch_set.js
@@ -0,0 +1,28 @@
+
+/*
+Tipue Search 2.0
+Copyright (c) 2012 Tipue
+Tipue Search is released under the MIT License
+http://www.tipue.com/search
+*/
+
+
+var tipuesearch_stop_words = ["and", "be", "by", "do", "for", "he", "how", "if", "is", "it", "my", "not", "of", "or", "the", "to", "up", "what", "when"];
+
+var tipuesearch_replace = {"words": [
+ {"word": "tipua", replace_with: "tipue"},
+ {"word": "javscript", replace_with: "javascript"}
+]};
+
+var tipuesearch_stem = {"words": [
+ {"word": "e-mail", stem: "email"},
+ {"word": "javascript", stem: "script"},
+ {"word": "javascript", stem: "js"}
+]};
+
+/*
+Include the following variable listing the pages on your site if you're using Live mode
+*/
+
+var tipuesearch_pages = ["http://foo.com/", "http://foo.com/about/", "http://foo.com/blog/", "http://foo.com/tos/"];
+
diff --git a/nikola/plugins/task_localsearch/files/tipue_search.html b/nikola/plugins/task_localsearch/files/tipue_search.html
new file mode 100755
index 0000000..789fbe5
--- /dev/null
+++ b/nikola/plugins/task_localsearch/files/tipue_search.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+
+<html>
+<head>
+<title>Tipue Search</title>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+
+<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
+
+<link rel="stylesheet" type="text/css" href="assets/css/tipuesearch.css">
+<script type="text/javascript" src="assets/js/tipuesearch_set.js"></script>
+<script type="text/javascript" src="assets/js/tipuesearch.js"></script>
+
+</head>
+<body>
+
+<div style="float: left;"><input type="text" id="tipue_search_input"></div>
+<div style="float: left; margin-left: 13px;"><input type="button" id="tipue_search_button"></div>
+<div id="tipue_search_content"><div id="tipue_search_loading"></div></div>
+</div>
+
+<script type="text/javascript">
+$(document).ready(function() {
+ $('#tipue_search_input').tipuesearch({
+ 'mode': 'json',
+ 'contentLocation': 'assets/js/tipuesearch_content.json'
+ });
+});
+</script>
+</body>
+</html>
diff --git a/nikola/plugins/task_mustache.plugin b/nikola/plugins/task_mustache.plugin
new file mode 100644
index 0000000..6103936
--- /dev/null
+++ b/nikola/plugins/task_mustache.plugin
@@ -0,0 +1,10 @@
+[Core]
+Name = render_mustache
+Module = task_mustache
+
+[Documentation]
+Author = Roberto Alsina
+Version = 0.1
+Website = http://nikola.ralsina.com.ar
+Description = Generates the blog's index pages in json.
+
diff --git a/nikola/plugins/task_mustache/__init__.py b/nikola/plugins/task_mustache/__init__.py
new file mode 100644
index 0000000..7364979
--- /dev/null
+++ b/nikola/plugins/task_mustache/__init__.py
@@ -0,0 +1,197 @@
+# Copyright (c) 2012 Roberto Alsina y otros.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+from __future__ import unicode_literals
+
+import codecs
+import json
+import os
+
+from nikola.plugin_categories import Task
+from nikola.utils import config_changed, copy_file, unicode_str
+
+
+class Mustache(Task):
+ """Render the blog posts as JSON data."""
+
+ name = "render_mustache"
+
+ def gen_tasks(self):
+ self.site.scan_posts()
+
+ kw = {
+ "translations": self.site.config['TRANSLATIONS'],
+ "index_display_post_count":
+ self.site.config['INDEX_DISPLAY_POST_COUNT'],
+ "messages": self.site.MESSAGES,
+ "index_teasers": self.site.config['INDEX_TEASERS'],
+ "output_folder": self.site.config['OUTPUT_FOLDER'],
+ "filters": self.site.config['FILTERS'],
+ "blog_title": self.site.config['BLOG_TITLE'],
+ "content_footer": self.site.config['CONTENT_FOOTER'],
+ }
+
+ # TODO: timeline is global, get rid of it
+ posts = [x for x in self.site.timeline if x.use_in_feeds]
+ if not posts:
+ yield {
+ 'basename': 'render_mustache',
+ 'actions': [],
+ }
+ return
+
+ def write_file(path, post, lang):
+
+ # Prev/Next links
+ prev_link = False
+ if post.prev_post:
+ prev_link = post.prev_post.permalink(lang).replace(".html",
+ ".json")
+ next_link = False
+ if post.next_post:
+ next_link = post.next_post.permalink(lang).replace(".html",
+ ".json")
+ data = {}
+
+ # Configuration
+ for k, v in self.site.config.items():
+ if isinstance(v, (str, unicode_str)): # NOQA
+ data[k] = v
+
+ # Tag data
+ tags = []
+ for tag in post.tags:
+ tags.append({'name': tag, 'link': self.site.link("tag", tag,
+ lang)})
+ data.update({
+ "tags": tags,
+ "tags?": True if tags else False,
+ })
+
+ # Template strings
+ for k, v in kw["messages"][lang].items():
+ data["message_" + k] = v
+
+ # Post data
+ data.update({
+ "title": post.title(lang),
+ "text": post.text(lang),
+ "prev": prev_link,
+ "next": next_link,
+ "date":
+ post.date.strftime(self.site.GLOBAL_CONTEXT['date_format']),
+ })
+
+ # Disqus comments
+ data["disqus_html"] = ('<div id="disqus_thread"></div> <script '
+ 'type="text/javascript">var disqus_'
+ 'shortname="%s";var disqus_url="%s";'
+ '(function(){var a=document.createElement'
+ '("script");a.type="text/javascript";'
+ 'a.async=true;a.src="http://"+disqus_'
+ 'shortname+".disqus.com/embed.js";('
+ 'document.getElementsByTagName("head")'
+ '[0]||document.getElementsByTagName("body")'
+ '[0]).appendChild(a)})(); </script>'
+ '<noscript>Please enable JavaScript to view'
+ ' the <a href="http://disqus.com/'
+ '?ref_noscript">comments powered by DISQUS.'
+ '</a></noscript><a href="http://disqus.com"'
+ 'class="dsq-brlink">comments powered by <sp'
+ 'an class="logo-disqus">DISQUS</span></a>' %
+ (self.site.config['DISQUS_FORUM'],
+ post.permalink(absolute=True)))
+
+ # Post translations
+ translations = []
+ for langname in kw["translations"]:
+ if langname == lang:
+ continue
+ translations.append({'name':
+ kw["messages"][langname]["Read in English"],
+ 'link': "javascript:load_data('%s');"
+ % post.permalink(langname).replace(
+ ".html", ".json")})
+ data["translations"] = translations
+
+ try:
+ os.makedirs(os.path.dirname(path))
+ except:
+ pass
+ with codecs.open(path, 'wb+', 'utf8') as fd:
+ fd.write(json.dumps(data))
+
+ for lang in kw["translations"]:
+ for i, post in enumerate(posts):
+ out_path = post.destination_path(lang, ".json")
+ out_file = os.path.join(kw['output_folder'], out_path)
+ task = {
+ 'basename': 'render_mustache',
+ 'name': out_file,
+ 'file_dep': post.fragment_deps(lang),
+ 'targets': [out_file],
+ 'actions': [(write_file, (out_file, post, lang))],
+ 'task_dep': ['render_posts'],
+ 'uptodate': [config_changed({
+ 1: post.text(lang),
+ 2: post.prev_post,
+ 3: post.next_post,
+ 4: post.title(lang),
+ })]
+ }
+ yield task
+
+ if posts:
+ first_post_data = posts[0].permalink(
+ self.site.config["DEFAULT_LANG"]).replace(".html", ".json")
+
+ # Copy mustache template
+ src = os.path.join(os.path.dirname(__file__), 'mustache-template.html')
+ dst = os.path.join(kw['output_folder'], 'mustache-template.html')
+ yield {
+ 'basename': 'render_mustache',
+ 'name': dst,
+ 'targets': [dst],
+ 'file_dep': [src],
+ 'actions': [(copy_file, (src, dst))],
+ }
+
+ # Copy mustache.html with the right starting file in it
+ src = os.path.join(os.path.dirname(__file__), 'mustache.html')
+ dst = os.path.join(kw['output_folder'], 'mustache.html')
+
+ def copy_mustache():
+ with codecs.open(src, 'rb', 'utf8') as in_file:
+ with codecs.open(dst, 'wb+', 'utf8') as out_file:
+ data = in_file.read().replace('{{first_post_data}}',
+ first_post_data)
+ out_file.write(data)
+ yield {
+ 'basename': 'render_mustache',
+ 'name': dst,
+ 'targets': [dst],
+ 'file_dep': [src],
+ 'uptodate': [config_changed({1: first_post_data})],
+ 'actions': [(copy_mustache, [])],
+ }
diff --git a/nikola/plugins/task_mustache/mustache-template.html b/nikola/plugins/task_mustache/mustache-template.html
new file mode 100644
index 0000000..7f2b34c
--- /dev/null
+++ b/nikola/plugins/task_mustache/mustache-template.html
@@ -0,0 +1,29 @@
+<script id="view" type="text/html">
+<div class="container" id="container">
+ <div class="postbox">
+ <h1>{{BLOG_TITLE}}</h1>
+ <hr>
+ <h2>{{title}}</h2>
+ Posted on: {{date}}</br>
+ {{#tags?}} More posts about:
+ {{#tags}}<a class="tag" href={{link}}><span class="badge badge-info">{{name}}</span></a>{{/tags}}
+ </br>
+ {{/tags?}}
+ {{#translations}}<a href={{link}}>{{name}}</a>{{/translations}}&nbsp;</br>
+ <hr>
+ {{{text}}}
+ <ul class="pager">
+ {{#prev}}
+ <li class="previous"><a href="javascript:load_data('{{prev}}')">{{message_Previous post}}</a></li>
+ {{/prev}}
+ {{#next}}
+ <li class="next"><a href="javascript:load_data('{{next}}')">{{message_Next post}}</a></li>
+ {{/next}}
+ </ul>
+ {{{disqus_html}}}
+ </div>
+ <div class="footerbox">
+ {{{CONTENT_FOOTER}}}
+ </div>
+</div>
+</script>
diff --git a/nikola/plugins/task_mustache/mustache.html b/nikola/plugins/task_mustache/mustache.html
new file mode 100644
index 0000000..5dbebef
--- /dev/null
+++ b/nikola/plugins/task_mustache/mustache.html
@@ -0,0 +1,36 @@
+<head>
+ <link href="/assets/css/bootstrap.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/bootstrap-responsive.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/rst.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/code.css" rel="stylesheet" type="text/css">
+ <link href="/assets/css/colorbox.css" rel="stylesheet" type="text/css"/>
+ <link href="/assets/css/slides.css" rel="stylesheet" type="text/css"/>
+ <link href="/assets/css/theme.css" rel="stylesheet" type="text/css"/>
+ <link href="/assets/css/custom.css" rel="stylesheet" type="text/css">
+ <script src="/assets/js/jquery-1.7.2.min.js" type="text/javascript"></script>
+ <script src="https://raw.github.com/jonnyreeves/jquery-Mustache/master/src/jquery.mustache.js"></script>
+ <script src="https://raw.github.com/janl/mustache.js/master/mustache.js"></script>
+ <script src="/assets/js/jquery.colorbox-min.js" type="text/javascript"></script>
+ <script src="/assets/js/slides.min.jquery.js" type="text/javascript"></script>
+ <script type="text/javascript">
+function load_data(dataurl) {
+ jQuery.getJSON(dataurl, function(data) {
+ $('body').mustache('view', data, { method: 'html' });
+ window.location.hash = '#' + dataurl;
+ })
+};
+$(document).ready(function() {
+$.Mustache.load('/mustache-template.html')
+ .done(function () {
+ if (window.location.hash != '') {
+ load_data(window.location.hash.slice(1));
+ }
+ else {
+ load_data('{{first_post_data}}');
+ };
+ })
+});
+</script>
+</head>
+<body style="padding-top: 0;">
+</body>
diff --git a/nikola/plugins/task_redirect.py b/nikola/plugins/task_redirect.py
index e440c30..2503bb7 100644
--- a/nikola/plugins/task_redirect.py
+++ b/nikola/plugins/task_redirect.py
@@ -57,7 +57,7 @@ class Redirect(Task):
src_path = os.path.join(kw["output_folder"], src)
yield {
'basename': self.name,
- 'name': src_path.encode('utf8'),
+ 'name': src_path,
'targets': [src_path],
'actions': [(create_redirect, (src_path, dst))],
'clean': True,
diff --git a/nikola/plugins/task_render_galleries.py b/nikola/plugins/task_render_galleries.py
index 0880e3e..d4e4a3a 100644
--- a/nikola/plugins/task_render_galleries.py
+++ b/nikola/plugins/task_render_galleries.py
@@ -62,6 +62,7 @@ class Galleries(Task):
'default_lang': self.site.config['DEFAULT_LANG'],
'blog_description': self.site.config['BLOG_DESCRIPTION'],
'use_filename_as_title': self.site.config['USE_FILENAME_AS_TITLE'],
+ 'gallery_path': self.site.config['GALLERY_PATH']
}
# FIXME: lots of work is done even when images don't change,
@@ -70,7 +71,7 @@ class Galleries(Task):
template_name = "gallery.tmpl"
gallery_list = []
- for root, dirs, files in os.walk('galleries'):
+ for root, dirs, files in os.walk(kw['gallery_path']):
gallery_list.append(root)
if not gallery_list:
yield {
@@ -95,7 +96,7 @@ class Galleries(Task):
if not os.path.isdir(output_gallery):
yield {
'basename': str('render_galleries'),
- 'name': str(output_gallery),
+ 'name': output_gallery,
'actions': [(os.makedirs, (output_gallery,))],
'targets': [output_gallery],
'clean': True,
@@ -152,7 +153,7 @@ class Galleries(Task):
thumbs.append(os.path.basename(thumb_path))
yield {
'basename': str('render_galleries'),
- 'name': thumb_path.encode('utf8'),
+ 'name': thumb_path,
'file_dep': [img],
'targets': [thumb_path],
'actions': [
@@ -164,7 +165,7 @@ class Galleries(Task):
}
yield {
'basename': str('render_galleries'),
- 'name': orig_dest_path.encode('utf8'),
+ 'name': orig_dest_path,
'file_dep': [img],
'targets': [orig_dest_path],
'actions': [
@@ -187,7 +188,7 @@ class Galleries(Task):
excluded_dest_path = os.path.join(output_gallery, img_name)
yield {
'basename': str('render_galleries_clean'),
- 'name': excluded_thumb_dest_path.encode('utf8'),
+ 'name': excluded_thumb_dest_path,
'file_dep': [exclude_path],
#'targets': [excluded_thumb_dest_path],
'actions': [
@@ -198,7 +199,7 @@ class Galleries(Task):
}
yield {
'basename': str('render_galleries_clean'),
- 'name': excluded_dest_path.encode('utf8'),
+ 'name': excluded_dest_path,
'file_dep': [exclude_path],
#'targets': [excluded_dest_path],
'actions': [
@@ -240,7 +241,7 @@ class Galleries(Task):
compile_html = self.site.get_compiler(index_path)
yield {
'basename': str('render_galleries'),
- 'name': index_dst_path.encode('utf-8'),
+ 'name': index_dst_path,
'file_dep': [index_path],
'targets': [index_dst_path],
'actions': [(compile_html, [index_path, index_dst_path])],
@@ -258,12 +259,11 @@ class Galleries(Task):
file_dep.append(index_dst_path)
else:
context['text'] = ''
- self.site.render_template(template_name, output_name.encode(
- 'utf8'), context)
+ self.site.render_template(template_name, output_name, context)
yield {
'basename': str('render_galleries'),
- 'name': output_name.encode('utf8'),
+ 'name': output_name,
'file_dep': file_dep,
'targets': [output_name],
'actions': [(render_gallery, (output_name, context,
@@ -303,8 +303,13 @@ class Galleries(Task):
break
- im.thumbnail(size, Image.ANTIALIAS)
- im.save(dst)
+ try:
+ im.thumbnail(size, Image.ANTIALIAS)
+ except Exception:
+ # TODO: inform the user, but do not fail
+ pass
+ else:
+ im.save(dst)
else:
utils.copy_file(src, dst)
diff --git a/nikola/plugins/task_render_listings.py b/nikola/plugins/task_render_listings.py
index b115a2f..0cadfd3 100644
--- a/nikola/plugins/task_render_listings.py
+++ b/nikola/plugins/task_render_listings.py
@@ -78,7 +78,7 @@ class Listings(Task):
'files': files,
'description': title,
}
- self.site.render_template('listing.tmpl', out_name.encode('utf8'),
+ self.site.render_template('listing.tmpl', out_name,
context)
flag = True
template_deps = self.site.template_system.template_deps('listing.tmpl')
@@ -91,14 +91,15 @@ class Listings(Task):
)
yield {
'basename': self.name,
- 'name': out_name.encode('utf8'),
+ 'name': out_name,
'file_dep': template_deps,
'targets': [out_name],
'actions': [(render_listing, [None, out_name, dirs, files])],
# This is necessary to reflect changes in blog title,
# sidebar links, etc.
'uptodate': [utils.config_changed(
- self.site.config['GLOBAL_CONTEXT'])]
+ self.site.config['GLOBAL_CONTEXT'])],
+ 'clean': True,
}
for f in files:
ext = os.path.splitext(f)[-1]
@@ -111,14 +112,15 @@ class Listings(Task):
f) + '.html'
yield {
'basename': self.name,
- 'name': out_name.encode('utf8'),
+ 'name': out_name,
'file_dep': template_deps + [in_name],
'targets': [out_name],
'actions': [(render_listing, [in_name, out_name])],
# This is necessary to reflect changes in blog title,
# sidebar links, etc.
'uptodate': [utils.config_changed(
- self.site.config['GLOBAL_CONTEXT'])]
+ self.site.config['GLOBAL_CONTEXT'])],
+ 'clean': True,
}
if flag:
yield {
diff --git a/nikola/plugins/task_render_pages.py b/nikola/plugins/task_render_pages.py
index 0145579..1883d7b 100644
--- a/nikola/plugins/task_render_pages.py
+++ b/nikola/plugins/task_render_pages.py
@@ -22,6 +22,7 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+from __future__ import unicode_literals
from nikola.plugin_categories import Task
from nikola.utils import config_changed
@@ -37,17 +38,21 @@ class RenderPages(Task):
"post_pages": self.site.config["post_pages"],
"translations": self.site.config["TRANSLATIONS"],
"filters": self.site.config["FILTERS"],
+ "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
}
self.site.scan_posts()
flag = False
for lang in kw["translations"]:
for post in self.site.timeline:
+ if kw["hide_untranslated_posts"] and not post.is_translation_available(lang):
+ continue
for task in self.site.generic_page_renderer(lang, post,
kw["filters"]):
task['uptodate'] = [config_changed({
1: task['uptodate'][0].config,
2: kw})]
task['basename'] = self.name
+ task['task_dep'] = ['render_posts']
flag = True
yield task
if flag is False: # No page rendered, yield a dummy task
diff --git a/nikola/plugins/task_render_posts.py b/nikola/plugins/task_render_posts.py
index a4d5578..4be68bf 100644
--- a/nikola/plugins/task_render_posts.py
+++ b/nikola/plugins/task_render_posts.py
@@ -23,10 +23,20 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
from copy import copy
-import os
+import codecs
+import string
from nikola.plugin_categories import Task
-from nikola import utils
+from nikola import utils, rc4
+
+
+def wrap_encrypt(path, password):
+ """Wrap a post with encryption."""
+ with codecs.open(path, 'rb+', 'utf8') as inf:
+ data = inf.read() + "<!--tail-->"
+ data = CRYPT.substitute(data=rc4.rc4(password, data))
+ with codecs.open(path, 'wb+', 'utf8') as outf:
+ outf.write(data)
class RenderPosts(Task):
@@ -41,25 +51,26 @@ class RenderPosts(Task):
"translations": self.site.config["TRANSLATIONS"],
"timeline": self.site.timeline,
"default_lang": self.site.config["DEFAULT_LANG"],
+ "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
}
flag = False
for lang in kw["translations"]:
- # TODO: timeline is global, get rid of it
deps_dict = copy(kw)
deps_dict.pop('timeline')
for post in kw['timeline']:
source = post.source_path
dest = post.base_path
- if lang != kw["default_lang"]:
- dest += '.' + lang
- source_lang = source + '.' + lang
- if os.path.exists(source_lang):
- source = source_lang
+ if not post.is_translation_available(lang) and kw["hide_untranslated_posts"]:
+ continue
+ else:
+ source = post.translated_source_path(lang)
+ if lang != post.default_lang:
+ dest = dest + '.' + lang
flag = True
- yield {
+ task = {
'basename': self.name,
- 'name': dest.encode('utf-8'),
+ 'name': dest,
'file_dep': post.fragment_deps(lang),
'targets': [dest],
'actions': [(self.site.get_compiler(post.source_path),
@@ -67,6 +78,9 @@ class RenderPosts(Task):
'clean': True,
'uptodate': [utils.config_changed(deps_dict)],
}
+ if post.meta('password'):
+ task['actions'].append((wrap_encrypt, (dest, post.meta('password'))))
+ yield task
if flag is False: # Return a dummy task
yield {
'basename': self.name,
@@ -74,3 +88,53 @@ class RenderPosts(Task):
'uptodate': [True],
'actions': [],
}
+
+
+CRYPT = string.Template("""\
+<script>
+function rc4(key, str) {
+ var s = [], j = 0, x, res = '';
+ for (var i = 0; i < 256; i++) {
+ s[i] = i;
+ }
+ for (i = 0; i < 256; i++) {
+ j = (j + s[i] + key.charCodeAt(i % key.length)) % 256;
+ x = s[i];
+ s[i] = s[j];
+ s[j] = x;
+ }
+ i = 0;
+ j = 0;
+ for (var y = 0; y < str.length; y++) {
+ i = (i + 1) % 256;
+ j = (j + s[i]) % 256;
+ x = s[i];
+ s[i] = s[j];
+ s[j] = x;
+ res += String.fromCharCode(str.charCodeAt(y) ^ s[(s[i] + s[j]) % 256]);
+ }
+ return res;
+}
+function decrypt() {
+ key = $$("#key").val();
+ crypt_div = $$("#encr")
+ crypted = crypt_div.html();
+ decrypted = rc4(key, window.atob(crypted));
+ if (decrypted.substr(decrypted.length - 11) == "<!--tail-->"){
+ crypt_div.html(decrypted);
+ $$("#pwform").hide();
+ crypt_div.show();
+ } else { alert("Wrong password"); };
+}
+</script>
+
+<div id="encr" style="display: none;">${data}</div>
+<div id="pwform">
+<form onsubmit="javascript:decrypt(); return false;" class="form-inline">
+<fieldset>
+<legend>This post is password-protected.</legend>
+<input type="password" id="key" placeholder="Type password here">
+<button type="submit" class="btn">Show Content</button>
+</fieldset>
+</form>
+</div>""")
diff --git a/nikola/plugins/task_render_rss.py b/nikola/plugins/task_render_rss.py
index 9ce1d63..3000e47 100644
--- a/nikola/plugins/task_render_rss.py
+++ b/nikola/plugins/task_render_rss.py
@@ -43,25 +43,30 @@ class RenderRSS(Task):
"blog_description": self.site.config["BLOG_DESCRIPTION"],
"output_folder": self.site.config["OUTPUT_FOLDER"],
"rss_teasers": self.site.config["RSS_TEASERS"],
+ "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
}
self.site.scan_posts()
- # TODO: timeline is global, kill it
for lang in kw["translations"]:
output_name = os.path.join(kw['output_folder'],
self.site.path("rss", None, lang))
deps = []
- posts = [x for x in self.site.timeline if x.use_in_feeds][:10]
+ if kw["hide_untranslated_posts"]:
+ posts = [x for x in self.site.timeline if x.use_in_feeds
+ and x.is_translation_available(lang)][:10]
+ else:
+ posts = [x for x in self.site.timeline if x.use_in_feeds][:10]
for post in posts:
deps += post.deps(lang)
yield {
'basename': 'render_rss',
- 'name': output_name.encode('utf8'),
+ 'name': os.path.normpath(output_name),
'file_dep': deps,
'targets': [output_name],
'actions': [(utils.generic_rss_renderer,
(lang, kw["blog_title"], kw["site_url"],
kw["blog_description"], posts, output_name,
kw["rss_teasers"]))],
+ 'task_dep': ['render_posts'],
'clean': True,
'uptodate': [utils.config_changed(kw)],
}
diff --git a/nikola/plugins/task_render_sources.py b/nikola/plugins/task_render_sources.py
index 529e68e..392345c 100644
--- a/nikola/plugins/task_render_sources.py
+++ b/nikola/plugins/task_render_sources.py
@@ -53,6 +53,8 @@ class Sources(Task):
flag = False
for lang in kw["translations"]:
for post in self.site.timeline:
+ if post.meta('password'):
+ continue
output_name = os.path.join(
kw['output_folder'], post.destination_path(
lang, post.source_ext()))
@@ -63,15 +65,16 @@ class Sources(Task):
source_lang = source + '.' + lang
if os.path.exists(source_lang):
source = source_lang
- yield {
- 'basename': 'render_sources',
- 'name': output_name.encode('utf8'),
- 'file_dep': [source],
- 'targets': [output_name],
- 'actions': [(utils.copy_file, (source, output_name))],
- 'clean': True,
- 'uptodate': [utils.config_changed(kw)],
- }
+ if os.path.isfile(source):
+ yield {
+ 'basename': 'render_sources',
+ 'name': os.path.normpath(output_name),
+ 'file_dep': [source],
+ 'targets': [output_name],
+ 'actions': [(utils.copy_file, (source, output_name))],
+ 'clean': True,
+ 'uptodate': [utils.config_changed(kw)],
+ }
if flag is False: # No page rendered, yield a dummy task
yield {
'basename': 'render_sources',
diff --git a/nikola/plugins/task_render_tags.py b/nikola/plugins/task_render_tags.py
index 744f0cb..58a7ff3 100644
--- a/nikola/plugins/task_render_tags.py
+++ b/nikola/plugins/task_render_tags.py
@@ -52,6 +52,7 @@ class RenderTags(Task):
self.site.config['INDEX_DISPLAY_POST_COUNT'],
"index_teasers": self.site.config['INDEX_TEASERS'],
"rss_teasers": self.site.config["RSS_TEASERS"],
+ "hide_untranslated_posts": self.site.config['HIDE_UNTRANSLATED_POSTS'],
}
self.site.scan_posts()
@@ -67,13 +68,17 @@ class RenderTags(Task):
post_list.sort(key=lambda a: a.date)
post_list.reverse()
for lang in kw["translations"]:
- yield self.tag_rss(tag, lang, posts, kw)
-
+ if kw["hide_untranslated_posts"]:
+ filtered_posts = [x for x in post_list if x.is_translation_available(lang)]
+ else:
+ filtered_posts = post_list
+ rss_post_list = [p.post_name for p in filtered_posts]
+ yield self.tag_rss(tag, lang, rss_post_list, kw)
# Render HTML
if kw['tag_pages_are_indexes']:
- yield self.tag_page_as_index(tag, lang, post_list, kw)
+ yield self.tag_page_as_index(tag, lang, filtered_posts, kw)
else:
- yield self.tag_page_as_list(tag, lang, post_list, kw)
+ yield self.tag_page_as_list(tag, lang, filtered_posts, kw)
# Tag cloud json file
tag_cloud_data = {}
@@ -98,6 +103,7 @@ class RenderTags(Task):
task['uptodate'] = [utils.config_changed(tag_cloud_data)]
task['targets'] = [output_name]
task['actions'] = [(write_tag_data, [tag_cloud_data])]
+ task['clean'] = True
yield task
def list_tags_page(self, kw):
@@ -110,7 +116,7 @@ class RenderTags(Task):
for lang in kw["translations"]:
output_name = os.path.join(
kw['output_folder'], self.site.path('tag_index', None, lang))
- output_name = output_name.encode('utf8')
+ output_name = output_name
context = {}
context["title"] = kw["messages"][lang]["Tags"]
context["items"] = [(tag, self.site.link("tag", tag, lang)) for tag
@@ -157,7 +163,6 @@ class RenderTags(Task):
context['rss_link'] = rss_link
output_name = os.path.join(kw['output_folder'],
page_name(tag, i, lang))
- output_name = output_name.encode('utf8')
context["title"] = kw["messages"][lang][
"Posts about %s"] % tag
context["prevlink"] = None
@@ -192,7 +197,6 @@ class RenderTags(Task):
template_name = "tag.tmpl"
output_name = os.path.join(kw['output_folder'], self.site.path(
"tag", tag, lang))
- output_name = output_name.encode('utf8')
context = {}
context["lang"] = lang
context["title"] = kw["messages"][lang]["Posts about %s"] % tag
@@ -217,7 +221,6 @@ class RenderTags(Task):
#Render RSS
output_name = os.path.join(kw['output_folder'],
self.site.path("tag_rss", tag, lang))
- output_name = output_name.encode('utf8')
deps = []
post_list = [self.site.global_data[post] for post in posts if
self.site.global_data[post].use_in_feeds]
@@ -236,4 +239,5 @@ class RenderTags(Task):
output_name, kw["rss_teasers"]))],
'clean': True,
'uptodate': [utils.config_changed(kw)],
+ 'task_dep': ['render_posts'],
}
diff --git a/nikola/plugins/task_sitemap/__init__.py b/nikola/plugins/task_sitemap/__init__.py
index 9d89070..044e0e3 100644
--- a/nikola/plugins/task_sitemap/__init__.py
+++ b/nikola/plugins/task_sitemap/__init__.py
@@ -22,72 +22,82 @@
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-from __future__ import print_function, absolute_import
+from __future__ import print_function, absolute_import, unicode_literals
+import codecs
+import datetime
import os
-import sys
-import tempfile
+try:
+ from urlparse import urljoin
+except ImportError:
+ from urllib.parse import urljoin # NOQA
from nikola.plugin_categories import LateTask
from nikola.utils import config_changed
-from nikola.plugins.task_sitemap import sitemap_gen
+
+header = """<?xml version="1.0" encoding="UTF-8"?>
+<urlset
+ xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+ 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">
+"""
+
+url_format = """ <url>
+ <loc>{0}</loc>
+ <lastmod>{1}</lastmod>
+ <priority>0.5000</priority>
+ </url>
+"""
+
+get_lastmod = lambda p: datetime.datetime.fromtimestamp(os.stat(p).st_mtime).isoformat().split('T')[0]
class Sitemap(LateTask):
- """Copy theme assets into output."""
+ """Generate google sitemap."""
name = "sitemap"
def gen_tasks(self):
- if sys.version_info[0] == 3:
- print("sitemap generation is not available for python 3")
- yield {
- 'basename': 'sitemap',
- 'name': 'sitemap',
- 'actions': [],
- }
- return
"""Generate Google sitemap."""
kw = {
"base_url": self.site.config["BASE_URL"],
"site_url": self.site.config["SITE_URL"],
"output_folder": self.site.config["OUTPUT_FOLDER"],
+ "mapped_extensions": self.site.config.get('MAPPED_EXTENSIONS', ['.html', '.htm'])
}
- output_path = os.path.abspath(kw['output_folder'])
- sitemap_path = os.path.join(output_path, "sitemap.xml.gz")
+ output_path = kw['output_folder']
+ sitemap_path = os.path.join(output_path, "sitemap.xml")
def sitemap():
- # Generate config
- config_data = """<?xml version="1.0" encoding="UTF-8"?>
- <site
- base_url="{0}"
- store_into="{1}"
- verbose="1" >
- <directory path="{2}" url="{3}" />
- <filter action="drop" type="wildcard" pattern="*~" />
- <filter action="drop" type="regexp" pattern="/\.[^/]*" />
- </site>""".format(kw["site_url"], sitemap_path, output_path,
- kw["base_url"])
- config_file = tempfile.NamedTemporaryFile(delete=False)
- config_file.write(config_data.encode('utf8'))
- config_file.close()
+ with codecs.open(sitemap_path, 'wb+', 'utf8') as outf:
+ output = kw['output_folder']
+ base_url = kw['base_url']
+ mapped_exts = kw['mapped_extensions']
+ outf.write(header)
+ locs = {}
+ for root, dirs, files in os.walk(output):
+ path = os.path.relpath(root, output)
+ path = path.replace(os.sep, '/') + '/'
+ lastmod = get_lastmod(root)
+ loc = urljoin(base_url, path)
+ locs[loc] = url_format.format(loc, lastmod)
+ for fname in files:
+ if os.path.splitext(fname)[-1] in mapped_exts:
+ real_path = os.path.join(root, fname)
+ path = os.path.relpath(real_path, output)
+ path = path.replace(os.sep, '/')
+ lastmod = get_lastmod(real_path)
+ loc = urljoin(base_url, path)
+ locs[loc] = url_format.format(loc, lastmod)
- # Generate sitemap
- sitemap = sitemap_gen.CreateSitemapFromFile(config_file.name, True)
- if not sitemap:
- sitemap_gen.output.Log('Configuration file errors -- exiting.',
- 0)
- else:
- sitemap.Generate()
- sitemap_gen.output.Log('Number of errors: {0}'.format(
- sitemap_gen.output.num_errors), 1)
- sitemap_gen.output.Log('Number of warnings: {0}'.format(
- sitemap_gen.output.num_warns), 1)
- os.unlink(config_file.name)
+ for k in sorted(locs.keys()):
+ outf.write(locs[k])
+ outf.write("</urlset>")
yield {
"basename": "sitemap",
- "name": os.path.join(kw['output_folder'], "sitemap.xml.gz"),
+ "name": sitemap_path,
"targets": [sitemap_path],
"actions": [(sitemap,)],
"uptodate": [config_changed(kw)],
diff --git a/nikola/plugins/task_sitemap/sitemap_gen.py b/nikola/plugins/task_sitemap/sitemap_gen.py
deleted file mode 100644
index 898325a..0000000
--- a/nikola/plugins/task_sitemap/sitemap_gen.py
+++ /dev/null
@@ -1,2137 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright (c) 2004, 2005 Google Inc.
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-#
-# * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-#
-# * Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in
-# the documentation and/or other materials provided with the
-# distribution.
-#
-# * Neither the name of Google nor the names of its contributors may
-# be used to endorse or promote products derived from this software
-# without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
-# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
-# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
-# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-# POSSIBILITY OF SUCH DAMAGE.
-#
-#
-# The sitemap_gen.py script is written in Python 2.2 and released to
-# the open source community for continuous improvements under the BSD
-# 2.0 new license, which can be found at:
-#
-# http://www.opensource.org/licenses/bsd-license.php
-#
-from __future__ import print_function
-
-__usage__ = \
- """A simple script to automatically produce sitemaps for a webserver,
-in the Google Sitemap Protocol (GSP).
-
-Usage: python sitemap_gen.py --config=config.xml [--help] [--testing]
- --config=config.xml, specifies config file location
- --help, displays usage message
- --testing, specified when user is experimenting
-"""
-
-import fnmatch
-import glob
-import gzip
-import os
-import re
-import stat
-import sys
-import time
-import urllib
-import xml.sax
-
-try:
- import md5
-except ImportError:
- md5 = None # NOQA
- import hashlib
-
-try:
- from urlparse import urlsplit, urlunsplit, urljoin
-except ImportError:
- from urllib.parse import urlsplit, urlunsplit, urljoin # NOQA
-
-try:
- from urllib import quote as urllib_quote
- from urllib import FancyURLopener
- from urllib import urlopen
-except ImportError:
- from urllib.parse import quote as urllib_quote # NOQA
- from urllib.request import FancyURLopener # NOQA
- from urllib.request import urlopen # NOQA
-
-
-if sys.version_info[0] == 3:
- # Python 3
- bytes_str = bytes
- unicode_str = str
- unichr = chr
-else:
- bytes_str = str
- unicode_str = unicode # NOQA
-
-# Text encodings
-ENC_ASCII = 'ASCII'
-ENC_UTF8 = 'UTF-8'
-ENC_IDNA = 'IDNA'
-ENC_ASCII_LIST = ['ASCII', 'US-ASCII', 'US', 'IBM367', 'CP367', 'ISO646-US'
- 'ISO_646.IRV:1991', 'ISO-IR-6', 'ANSI_X3.4-1968',
- 'ANSI_X3.4-1986', 'CPASCII']
-ENC_DEFAULT_LIST = ['ISO-8859-1', 'ISO-8859-2', 'ISO-8859-5']
-
-# Available Sitemap types
-SITEMAP_TYPES = ['web', 'mobile', 'news']
-
-# General Sitemap tags
-GENERAL_SITEMAP_TAGS = ['loc', 'changefreq', 'priority', 'lastmod']
-
-# News specific tags
-NEWS_SPECIFIC_TAGS = ['keywords', 'publication_date', 'stock_tickers']
-
-# News Sitemap tags
-NEWS_SITEMAP_TAGS = GENERAL_SITEMAP_TAGS + NEWS_SPECIFIC_TAGS
-
-# Maximum number of urls in each sitemap, before next Sitemap is created
-MAXURLS_PER_SITEMAP = 50000
-
-# Suffix on a Sitemap index file
-SITEINDEX_SUFFIX = '_index.xml'
-
-# Regular expressions tried for extracting URLs from access logs.
-ACCESSLOG_CLF_PATTERN = re.compile(
- r'.+\s+"([^\s]+)\s+([^\s]+)\s+HTTP/\d+\.\d+"\s+200\s+.*'
-)
-
-# Match patterns for lastmod attributes
-DATE_PATTERNS = list(map(re.compile, [
- r'^\d\d\d\d$',
- r'^\d\d\d\d-\d\d$',
- r'^\d\d\d\d-\d\d-\d\d$',
- r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\dZ$',
- r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\d[+-]\d\d:\d\d$',
- r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?Z$',
- r'^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?[+-]\d\d:\d\d$',
-]))
-
-# Match patterns for changefreq attributes
-CHANGEFREQ_PATTERNS = [
- 'always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'
-]
-
-# XML formats
-GENERAL_SITEINDEX_HEADER = \
- '<?xml version="1.0" encoding="UTF-8"?>\n' \
- '<sitemapindex\n' \
- ' xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"\n' \
- ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' \
- ' xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9\n' \
- ' http://www.sitemaps.org/schemas/sitemap/0.9/' \
- 'siteindex.xsd">\n'
-
-NEWS_SITEINDEX_HEADER = \
- '<?xml version="1.0" encoding="UTF-8"?>\n' \
- '<sitemapindex\n' \
- ' xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"\n' \
- ' xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"\n' \
- ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' \
- ' xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9\n' \
- ' http://www.sitemaps.org/schemas/sitemap/0.9/' \
- 'siteindex.xsd">\n'
-
-SITEINDEX_FOOTER = '</sitemapindex>\n'
-SITEINDEX_ENTRY = \
- ' <sitemap>\n' \
- ' <loc>%(loc)s</loc>\n' \
- ' <lastmod>%(lastmod)s</lastmod>\n' \
- ' </sitemap>\n'
-GENERAL_SITEMAP_HEADER = \
- '<?xml version="1.0" encoding="UTF-8"?>\n' \
- '<urlset\n' \
- ' xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"\n' \
- ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' \
- ' xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9\n' \
- ' http://www.sitemaps.org/schemas/sitemap/0.9/' \
- 'sitemap.xsd">\n'
-
-NEWS_SITEMAP_HEADER = \
- '<?xml version="1.0" encoding="UTF-8"?>\n' \
- '<urlset\n' \
- ' xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"\n' \
- ' xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"\n' \
- ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n' \
- ' xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9\n' \
- ' http://www.sitemaps.org/schemas/sitemap/0.9/' \
- 'sitemap.xsd">\n'
-
-SITEMAP_FOOTER = '</urlset>\n'
-SITEURL_XML_PREFIX = ' <url>\n'
-SITEURL_XML_SUFFIX = ' </url>\n'
-
-NEWS_TAG_XML_PREFIX = ' <news:news>\n'
-NEWS_TAG_XML_SUFFIX = ' </news:news>\n'
-
-# Search engines to notify with the updated sitemaps
-#
-# This list is very non-obvious in what's going on. Here's the gist:
-# Each item in the list is a 6-tuple of items. The first 5 are "almost"
-# the same as the input arguments to urlparse.urlunsplit():
-# 0 - schema
-# 1 - netloc
-# 2 - path
-# 3 - query <-- EXCEPTION: specify a query map rather than a string
-# 4 - fragment
-# Additionally, add item 5:
-# 5 - query attribute that should be set to the new Sitemap URL
-# Clear as mud, I know.
-NOTIFICATION_SITES = [
- ('http', 'www.google.com', 'webmasters/sitemaps/ping', {}, '', 'sitemap'),
-]
-
-
-def get_hash(text):
- if md5 is not None:
- return md5.new(text).digest()
- else:
- m = hashlib.md5()
- m.update(text.encode('utf8'))
- return m.digest()
-
-
-class Error(Exception):
- """
- Base exception class. In this module we tend not to use our own exception
- types for very much, but they come in very handy on XML parsing with SAX.
- """
- pass
-# end class Error
-
-
-class SchemaError(Error):
- """Failure to process an XML file according to the schema we know."""
- pass
-# end class SchemeError
-
-
-class Encoder:
- """
- Manages wide-character/narrow-character conversions for just about all
- text that flows into or out of the script.
-
- You should always use this class for string coercion, as opposed to
- letting Python handle coercions automatically. Reason: Python
- usually assumes ASCII (7-bit) as a default narrow character encoding,
- which is not the kind of data we generally deal with.
-
- General high-level methodologies used in sitemap_gen:
-
- [PATHS]
- File system paths may be wide or narrow, depending on platform.
- This works fine, just be aware of it and be very careful to not
- mix them. That is, if you have to pass several file path arguments
- into a library call, make sure they are all narrow or all wide.
- This class has MaybeNarrowPath() which should be called on every
- file system path you deal with.
-
- [URLS]
- URL locations are stored in Narrow form, already escaped. This has the
- benefit of keeping escaping and encoding as close as possible to the format
- we read them in. The downside is we may end up with URLs that have
- intermingled encodings -- the root path may be encoded in one way
- while the filename is encoded in another. This is obviously wrong, but
- it should hopefully be an issue hit by very few users. The workaround
- from the user level (assuming they notice) is to specify a default_encoding
- parameter in their config file.
-
- [OTHER]
- Other text, such as attributes of the URL class, configuration options,
- etc, are generally stored in Unicode for simplicity.
- """
-
- def __init__(self):
- self._user = None # User-specified default encoding
- self._learned = [] # Learned default encodings
- self._widefiles = False # File system can be wide
-
- # Can the file system be Unicode?
- try:
- self._widefiles = os.path.supports_unicode_filenames
- except AttributeError:
- try:
- self._widefiles = sys.getwindowsversion(
- ) == os.VER_PLATFORM_WIN32_NT
- except AttributeError:
- pass
-
- # Try to guess a working default
- try:
- encoding = sys.getfilesystemencoding()
- if encoding and not (encoding.upper() in ENC_ASCII_LIST):
- self._learned = [encoding]
- except AttributeError:
- pass
-
- if not self._learned:
- encoding = sys.getdefaultencoding()
- if encoding and not (encoding.upper() in ENC_ASCII_LIST):
- self._learned = [encoding]
-
- # If we had no guesses, start with some European defaults
- if not self._learned:
- self._learned = ENC_DEFAULT_LIST
- # end def __init__
-
- def SetUserEncoding(self, encoding):
- self._user = encoding
- # end def SetUserEncoding
-
- def NarrowText(self, text, encoding):
- """ Narrow a piece of arbitrary text """
- if isinstance(text, bytes_str):
- return text
-
- # Try the passed in preference
- if encoding:
- try:
- result = text.encode(encoding)
- if not encoding in self._learned:
- self._learned.append(encoding)
- return result
- except UnicodeError:
- pass
- except LookupError:
- output.Warn('Unknown encoding: %s' % encoding)
-
- # Try the user preference
- if self._user:
- try:
- return text.encode(self._user)
- except UnicodeError:
- pass
- except LookupError:
- temp = self._user
- self._user = None
- output.Warn('Unknown default_encoding: %s' % temp)
-
- # Look through learned defaults, knock any failing ones out of the list
- while self._learned:
- try:
- return text.encode(self._learned[0])
- except:
- del self._learned[0]
-
- # When all other defaults are exhausted, use UTF-8
- try:
- return text.encode(ENC_UTF8)
- except UnicodeError:
- pass
-
- # Something is seriously wrong if we get to here
- return text.encode(ENC_ASCII, 'ignore')
- # end def NarrowText
-
- def MaybeNarrowPath(self, text):
- """ Paths may be allowed to stay wide """
- if self._widefiles:
- return text
- return self.NarrowText(text, None)
- # end def MaybeNarrowPath
-
- def WidenText(self, text, encoding):
- """ Widen a piece of arbitrary text """
- if not isinstance(text, bytes_str):
- return text
-
- # Try the passed in preference
- if encoding:
- try:
- result = unicode_str(text, encoding)
- if not encoding in self._learned:
- self._learned.append(encoding)
- return result
- except UnicodeError:
- pass
- except LookupError:
- output.Warn('Unknown encoding: %s' % encoding)
-
- # Try the user preference
- if self._user:
- try:
- return unicode_str(text, self._user)
- except UnicodeError:
- pass
- except LookupError:
- temp = self._user
- self._user = None
- output.Warn('Unknown default_encoding: %s' % temp)
-
- # Look through learned defaults, knock any failing ones out of the list
- while self._learned:
- try:
- return unicode_str(text, self._learned[0])
- except:
- del self._learned[0]
-
- # When all other defaults are exhausted, use UTF-8
- try:
- return unicode_str(text, ENC_UTF8)
- except UnicodeError:
- pass
-
- # Getting here means it wasn't UTF-8 and we had no working default.
- # We really don't have anything "right" we can do anymore.
- output.Warn('Unrecognized encoding in text: %s' % text)
- if not self._user:
- output.Warn('You may need to set a default_encoding in your '
- 'configuration file.')
- return text.decode(ENC_ASCII, 'ignore')
- # end def WidenText
-# end class Encoder
-encoder = Encoder()
-
-
-class Output:
- """
- Exposes logging functionality, and tracks how many errors
- we have thus output.
-
- Logging levels should be used as thus:
- Fatal -- extremely sparingly
- Error -- config errors, entire blocks of user 'intention' lost
- Warn -- individual URLs lost
- Log(,0) -- Un-suppressable text that's not an error
- Log(,1) -- touched files, major actions
- Log(,2) -- parsing notes, filtered or duplicated URLs
- Log(,3) -- each accepted URL
- """
-
- def __init__(self):
- self.num_errors = 0 # Count of errors
- self.num_warns = 0 # Count of warnings
-
- self._errors_shown = {} # Shown errors
- self._warns_shown = {} # Shown warnings
- self._verbose = 0 # Level of verbosity
- # end def __init__
-
- def Log(self, text, level):
- """ Output a blurb of diagnostic text, if the verbose level allows it """
- if text:
- text = encoder.NarrowText(text, None)
- if self._verbose >= level:
- print(text)
- # end def Log
-
- def Warn(self, text):
- """ Output and count a warning. Suppress duplicate warnings. """
- if text:
- text = encoder.NarrowText(text, None)
- hash = get_hash(text)
- if not hash in self._warns_shown:
- self._warns_shown[hash] = 1
- print('[WARNING] ' + text)
- else:
- self.Log('(suppressed) [WARNING] ' + text, 3)
- self.num_warns = self.num_warns + 1
- # end def Warn
-
- def Error(self, text):
- """ Output and count an error. Suppress duplicate errors. """
- if text:
- text = encoder.NarrowText(text, None)
- hash = get_hash(text)
- if not hash in self._errors_shown:
- self._errors_shown[hash] = 1
- print('[ERROR] ' + text)
- else:
- self.Log('(suppressed) [ERROR] ' + text, 3)
- self.num_errors = self.num_errors + 1
- # end def Error
-
- def Fatal(self, text):
- """ Output an error and terminate the program. """
- if text:
- text = encoder.NarrowText(text, None)
- print('[FATAL] ' + text)
- else:
- print('Fatal error.')
- sys.exit(1)
- # end def Fatal
-
- def SetVerbose(self, level):
- """ Sets the verbose level. """
- try:
- if not isinstance(level, int):
- level = int(level)
- if (level >= 0) and (level <= 3):
- self._verbose = level
- return
- except ValueError:
- pass
- self.Error(
- 'Verbose level (%s) must be between 0 and 3 inclusive.' % level)
- # end def SetVerbose
-# end class Output
-output = Output()
-
-
-class URL(object):
- """ URL is a smart structure grouping together the properties we
- care about for a single web reference. """
- __slots__ = 'loc', 'lastmod', 'changefreq', 'priority'
-
- def __init__(self):
- self.loc = None # URL -- in Narrow characters
- self.lastmod = None # ISO8601 timestamp of last modify
- self.changefreq = None # Text term for update frequency
- self.priority = None # Float between 0 and 1 (inc)
- # end def __init__
-
- def __cmp__(self, other):
- if self.loc < other.loc:
- return -1
- if self.loc > other.loc:
- return 1
- return 0
- # end def __cmp__
-
- def TrySetAttribute(self, attribute, value):
- """ Attempt to set the attribute to the value, with a pretty try
- block around it. """
- if attribute == 'loc':
- self.loc = self.Canonicalize(value)
- else:
- try:
- setattr(self, attribute, value)
- except AttributeError:
- output.Warn('Unknown URL attribute: %s' % attribute)
- # end def TrySetAttribute
-
- def IsAbsolute(loc):
- """ Decide if the URL is absolute or not """
- if not loc:
- return False
- narrow = encoder.NarrowText(loc, None)
- (scheme, netloc, path, query, frag) = urlsplit(narrow)
- if (not scheme) or (not netloc):
- return False
- return True
- # end def IsAbsolute
- IsAbsolute = staticmethod(IsAbsolute)
-
- def Canonicalize(loc):
- """ Do encoding and canonicalization on a URL string """
- if not loc:
- return loc
-
- # Let the encoder try to narrow it
- narrow = encoder.NarrowText(loc, None)
-
- # Escape components individually
- (scheme, netloc, path, query, frag) = urlsplit(narrow)
- unr = '-._~'
- sub = '!$&\'()*+,;='
- netloc = urllib_quote(netloc, unr + sub + '%:@/[]')
- path = urllib_quote(path, unr + sub + '%:@/')
- query = urllib_quote(query, unr + sub + '%:@/?')
- frag = urllib_quote(frag, unr + sub + '%:@/?')
-
- # Try built-in IDNA encoding on the netloc
- try:
- (ignore, widenetloc, ignore, ignore, ignore) = urlsplit(loc)
- for c in widenetloc:
- if c >= unichr(128):
- netloc = widenetloc.encode(ENC_IDNA)
- netloc = urllib_quote(netloc, unr + sub + '%:@/[]')
- break
- except UnicodeError:
- # urlsplit must have failed, based on implementation differences in the
- # library. There is not much we can do here, except ignore it.
- pass
- except LookupError:
- output.Warn('An International Domain Name (IDN) is being used, but this '
- 'version of Python does not have support for IDNA encoding. '
- ' (IDNA support was introduced in Python 2.3) The encoding '
- 'we have used instead is wrong and will probably not yield '
- 'valid URLs.')
- bad_netloc = False
- if '%' in netloc:
- bad_netloc = True
-
- # Put it all back together
- narrow = urlunsplit((scheme, netloc, path, query, frag))
-
- # I let '%' through. Fix any that aren't pre-existing escapes.
- HEXDIG = '0123456789abcdefABCDEF'
- list = narrow.split('%')
- narrow = list[0]
- del list[0]
- for item in list:
- if (len(item) >= 2) and (item[0] in HEXDIG) and (item[1] in HEXDIG):
- narrow = narrow + '%' + item
- else:
- narrow = narrow + '%25' + item
-
- # Issue a warning if this is a bad URL
- if bad_netloc:
- output.Warn('Invalid characters in the host or domain portion of a URL: '
- + narrow)
-
- return narrow
- # end def Canonicalize
- Canonicalize = staticmethod(Canonicalize)
-
- def VerifyDate(self, date, metatag):
- """Verify the date format is valid"""
- match = False
- if date:
- date = date.upper()
- for pattern in DATE_PATTERNS:
- match = pattern.match(date)
- if match:
- return True
- if not match:
- output.Warn('The value for %s does not appear to be in ISO8601 '
- 'format on URL: %s' % (metatag, self.loc))
- return False
- # end of VerifyDate
-
- def Validate(self, base_url, allow_fragment):
- """ Verify the data in this URL is well-formed, and override if not. """
- assert isinstance(base_url, bytes_str)
-
- # Test (and normalize) the ref
- if not self.loc:
- output.Warn('Empty URL')
- return False
- if allow_fragment:
- self.loc = urljoin(base_url, self.loc)
- if not self.loc.startswith(base_url):
- output.Warn('Discarded URL for not starting with the base_url: %s' %
- self.loc)
- self.loc = None
- return False
-
- # Test the lastmod
- if self.lastmod:
- if not self.VerifyDate(self.lastmod, "lastmod"):
- self.lastmod = None
-
- # Test the changefreq
- if self.changefreq:
- match = False
- self.changefreq = self.changefreq.lower()
- for pattern in CHANGEFREQ_PATTERNS:
- if self.changefreq == pattern:
- match = True
- break
- if not match:
- output.Warn('Changefreq "%s" is not a valid change frequency on URL '
- ': %s' % (self.changefreq, self.loc))
- self.changefreq = None
-
- # Test the priority
- if self.priority:
- priority = -1.0
- try:
- priority = float(self.priority)
- except ValueError:
- pass
- if (priority < 0.0) or (priority > 1.0):
- output.Warn('Priority "%s" is not a number between 0 and 1 inclusive '
- 'on URL: %s' % (self.priority, self.loc))
- self.priority = None
-
- return True
- # end def Validate
-
- def MakeHash(self):
- """ Provides a uniform way of hashing URLs """
- if not self.loc:
- return None
- if self.loc.endswith('/'):
- return get_hash(self.loc[:-1])
- return get_hash(self.loc)
- # end def MakeHash
-
- def Log(self, prefix='URL', level=3):
- """ Dump the contents, empty or not, to the log. """
- out = prefix + ':'
-
- for attribute in self.__slots__:
- value = getattr(self, attribute)
- if not value:
- value = ''
- out = out + (' %s=[%s]' % (attribute, value))
-
- output.Log('%s' % encoder.NarrowText(out, None), level)
- # end def Log
-
- def WriteXML(self, file):
- """ Dump non-empty contents to the output file, in XML format. """
- if not self.loc:
- return
- out = SITEURL_XML_PREFIX
-
- for attribute in self.__slots__:
- value = getattr(self, attribute)
- if value:
- if isinstance(value, unicode_str):
- value = encoder.NarrowText(value, None)
- elif not isinstance(value, bytes_str):
- value = str(value)
- value = xml.sax.saxutils.escape(value)
- out = out + (' <%s>%s</%s>\n' % (attribute, value, attribute))
-
- out = out + SITEURL_XML_SUFFIX
- file.write(out)
- # end def WriteXML
-# end class URL
-
-
-class NewsURL(URL):
- """ NewsURL is a subclass of URL with News-Sitemap specific properties. """
- __slots__ = 'loc', 'lastmod', 'changefreq', 'priority', 'publication_date', \
- 'keywords', 'stock_tickers'
-
- def __init__(self):
- URL.__init__(self)
- self.publication_date = None # ISO8601 timestamp of publication date
- self.keywords = None # Text keywords
- self.stock_tickers = None # Text stock
- # end def __init__
-
- def Validate(self, base_url, allow_fragment):
- """ Verify the data in this News URL is well-formed, and override if not. """
- assert isinstance(base_url, bytes_str)
-
- if not URL.Validate(self, base_url, allow_fragment):
- return False
-
- if not URL.VerifyDate(self, self.publication_date, "publication_date"):
- self.publication_date = None
-
- return True
- # end def Validate
-
- def WriteXML(self, file):
- """ Dump non-empty contents to the output file, in XML format. """
- if not self.loc:
- return
- out = SITEURL_XML_PREFIX
-
- # printed_news_tag indicates if news-specific metatags are present
- printed_news_tag = False
- for attribute in self.__slots__:
- value = getattr(self, attribute)
- if value:
- if isinstance(value, unicode_str):
- value = encoder.NarrowText(value, None)
- elif not isinstance(value, bytes_str):
- value = str(value)
- value = xml.sax.saxutils.escape(value)
- if attribute in NEWS_SPECIFIC_TAGS:
- if not printed_news_tag:
- printed_news_tag = True
- out = out + NEWS_TAG_XML_PREFIX
- out = out + (' <news:%s>%s</news:%s>\n' %
- (attribute, value, attribute))
- else:
- out = out + (' <%s>%s</%s>\n' % (
- attribute, value, attribute))
-
- if printed_news_tag:
- out = out + NEWS_TAG_XML_SUFFIX
- out = out + SITEURL_XML_SUFFIX
- file.write(out)
- # end def WriteXML
-# end class NewsURL
-
-
-class Filter:
- """
- A filter on the stream of URLs we find. A filter is, in essence,
- a wildcard applied to the stream. You can think of this as an
- operator that returns a tri-state when given a URL:
-
- True -- this URL is to be included in the sitemap
- None -- this URL is undecided
- False -- this URL is to be dropped from the sitemap
- """
-
- def __init__(self, attributes):
- self._wildcard = None # Pattern for wildcard match
- self._regexp = None # Pattern for regexp match
- self._pass = False # "Drop" filter vs. "Pass" filter
-
- if not ValidateAttributes('FILTER', attributes,
- ('pattern', 'type', 'action')):
- return
-
- # Check error count on the way in
- num_errors = output.num_errors
-
- # Fetch the attributes
- pattern = attributes.get('pattern')
- type = attributes.get('type', 'wildcard')
- action = attributes.get('action', 'drop')
- if type:
- type = type.lower()
- if action:
- action = action.lower()
-
- # Verify the attributes
- if not pattern:
- output.Error('On a filter you must specify a "pattern" to match')
- elif (not type) or ((type != 'wildcard') and (type != 'regexp')):
- output.Error('On a filter you must specify either \'type="wildcard"\' '
- 'or \'type="regexp"\'')
- elif (action != 'pass') and (action != 'drop'):
- output.Error('If you specify a filter action, it must be either '
- '\'action="pass"\' or \'action="drop"\'')
-
- # Set the rule
- if action == 'drop':
- self._pass = False
- elif action == 'pass':
- self._pass = True
-
- if type == 'wildcard':
- self._wildcard = pattern
- elif type == 'regexp':
- try:
- self._regexp = re.compile(pattern)
- except re.error:
- output.Error('Bad regular expression: %s' % pattern)
-
- # Log the final results iff we didn't add any errors
- if num_errors == output.num_errors:
- output.Log('Filter: %s any URL that matches %s "%s"' %
- (action, type, pattern), 2)
- # end def __init__
-
- def Apply(self, url):
- """ Process the URL, as above. """
- if (not url) or (not url.loc):
- return None
-
- if self._wildcard:
- if fnmatch.fnmatchcase(url.loc, self._wildcard):
- return self._pass
- return None
-
- if self._regexp:
- if self._regexp.search(url.loc):
- return self._pass
- return None
-
- assert False # unreachable
- # end def Apply
-# end class Filter
-
-
-class InputURL:
- """
- Each Input class knows how to yield a set of URLs from a data source.
-
- This one handles a single URL, manually specified in the config file.
- """
-
- def __init__(self, attributes):
- self._url = None # The lonely URL
-
- if not ValidateAttributes('URL', attributes,
- ('href', 'lastmod', 'changefreq', 'priority')):
- return
-
- url = URL()
- for attr in attributes.keys():
- if attr == 'href':
- url.TrySetAttribute('loc', attributes[attr])
- else:
- url.TrySetAttribute(attr, attributes[attr])
-
- if not url.loc:
- output.Error('Url entries must have an href attribute.')
- return
-
- self._url = url
- output.Log('Input: From URL "%s"' % self._url.loc, 2)
- # end def __init__
-
- def ProduceURLs(self, consumer):
- """ Produces URLs from our data source, hands them in to the consumer. """
- if self._url:
- consumer(self._url, True)
- # end def ProduceURLs
-# end class InputURL
-
-
-class InputURLList:
- """
- Each Input class knows how to yield a set of URLs from a data source.
-
- This one handles a text file with a list of URLs
- """
-
- def __init__(self, attributes):
- self._path = None # The file path
- self._encoding = None # Encoding of that file
-
- if not ValidateAttributes('URLLIST', attributes, ('path', 'encoding')):
- return
-
- self._path = attributes.get('path')
- self._encoding = attributes.get('encoding', ENC_UTF8)
- if self._path:
- self._path = encoder.MaybeNarrowPath(self._path)
- if os.path.isfile(self._path):
- output.Log('Input: From URLLIST "%s"' % self._path, 2)
- else:
- output.Error('Can not locate file: %s' % self._path)
- self._path = None
- else:
- output.Error('Urllist entries must have a "path" attribute.')
- # end def __init__
-
- def ProduceURLs(self, consumer):
- """ Produces URLs from our data source, hands them in to the consumer. """
-
- # Open the file
- (frame, file) = OpenFileForRead(self._path, 'URLLIST')
- if not file:
- return
-
- # Iterate lines
- linenum = 0
- for line in file.readlines():
- linenum = linenum + 1
-
- # Strip comments and empty lines
- if self._encoding:
- line = encoder.WidenText(line, self._encoding)
- line = line.strip()
- if (not line) or line[0] == '#':
- continue
-
- # Split the line on space
- url = URL()
- cols = line.split(' ')
- for i in range(0, len(cols)):
- cols[i] = cols[i].strip()
- url.TrySetAttribute('loc', cols[0])
-
- # Extract attributes from the other columns
- for i in range(1, len(cols)):
- if cols[i]:
- try:
- (attr_name, attr_val) = cols[i].split('=', 1)
- url.TrySetAttribute(attr_name, attr_val)
- except ValueError:
- output.Warn('Line %d: Unable to parse attribute: %s' %
- (linenum, cols[i]))
-
- # Pass it on
- consumer(url, False)
-
- file.close()
- if frame:
- frame.close()
- # end def ProduceURLs
-# end class InputURLList
-
-
-class InputNewsURLList:
- """
- Each Input class knows how to yield a set of URLs from a data source.
-
- This one handles a text file with a list of News URLs and their metadata
- """
-
- def __init__(self, attributes):
- self._path = None # The file path
- self._encoding = None # Encoding of that file
- self._tag_order = [] # Order of URL metadata
-
- if not ValidateAttributes('URLLIST', attributes, ('path', 'encoding', 'tag_order')):
- return
-
- self._path = attributes.get('path')
- self._encoding = attributes.get('encoding', ENC_UTF8)
- self._tag_order = attributes.get('tag_order')
-
- if self._path:
- self._path = encoder.MaybeNarrowPath(self._path)
- if os.path.isfile(self._path):
- output.Log('Input: From URLLIST "%s"' % self._path, 2)
- else:
- output.Error('Can not locate file: %s' % self._path)
- self._path = None
- else:
- output.Error('Urllist entries must have a "path" attribute.')
-
- # parse tag_order into an array
- # tag_order_ascii created for more readable logging
- tag_order_ascii = []
- if self._tag_order:
- self._tag_order = self._tag_order.split(",")
- for i in range(0, len(self._tag_order)):
- element = self._tag_order[i].strip().lower()
- self._tag_order[i] = element
- tag_order_ascii.append(element.encode('ascii'))
- output.Log(
- 'Input: From URLLIST tag order is "%s"' % tag_order_ascii, 0)
- else:
- output.Error('News Urllist configuration file must contain tag_order '
- 'to define Sitemap metatags.')
-
- # verify all tag_order inputs are valid
- tag_order_dict = {}
- for tag in self._tag_order:
- tag_order_dict[tag] = ""
- if not ValidateAttributes('URLLIST', tag_order_dict,
- NEWS_SITEMAP_TAGS):
- return
-
- # loc tag must be present
- loc_tag = False
- for tag in self._tag_order:
- if tag == 'loc':
- loc_tag = True
- break
- if not loc_tag:
- output.Error('News Urllist tag_order in configuration file '
- 'does not contain "loc" value: %s' % tag_order_ascii)
- # end def __init__
-
- def ProduceURLs(self, consumer):
- """ Produces URLs from our data source, hands them in to the consumer. """
-
- # Open the file
- (frame, file) = OpenFileForRead(self._path, 'URLLIST')
- if not file:
- return
-
- # Iterate lines
- linenum = 0
- for line in file.readlines():
- linenum = linenum + 1
-
- # Strip comments and empty lines
- if self._encoding:
- line = encoder.WidenText(line, self._encoding)
- line = line.strip()
- if (not line) or line[0] == '#':
- continue
-
- # Split the line on tabs
- url = NewsURL()
- cols = line.split('\t')
- for i in range(0, len(cols)):
- cols[i] = cols[i].strip()
-
- for i in range(0, len(cols)):
- if cols[i]:
- attr_value = cols[i]
- if i < len(self._tag_order):
- attr_name = self._tag_order[i]
- try:
- url.TrySetAttribute(attr_name, attr_value)
- except ValueError:
- output.Warn('Line %d: Unable to parse attribute: %s' %
- (linenum, cols[i]))
-
- # Pass it on
- consumer(url, False)
-
- file.close()
- if frame:
- frame.close()
- # end def ProduceURLs
-# end class InputNewsURLList
-
-
-class InputDirectory:
- """
- Each Input class knows how to yield a set of URLs from a data source.
-
- This one handles a directory that acts as base for walking the filesystem.
- """
-
- def __init__(self, attributes, base_url):
- self._path = None # The directory
- self._url = None # The URL equivalent
- self._default_file = None
- self._remove_empty_directories = False
-
- if not ValidateAttributes('DIRECTORY', attributes, ('path', 'url',
- 'default_file', 'remove_empty_directories')):
- return
-
- # Prep the path -- it MUST end in a sep
- path = attributes.get('path')
- if not path:
- output.Error('Directory entries must have both "path" and "url" '
- 'attributes')
- return
- path = encoder.MaybeNarrowPath(path)
- if not path.endswith(os.sep):
- path = path + os.sep
- if not os.path.isdir(path):
- output.Error('Can not locate directory: %s' % path)
- return
-
- # Prep the URL -- it MUST end in a sep
- url = attributes.get('url')
- if not url:
- output.Error('Directory entries must have both "path" and "url" '
- 'attributes')
- return
- url = URL.Canonicalize(url)
- if not url.endswith('/'):
- url = url + '/'
- if not url.startswith(base_url):
- url = urljoin(base_url, url)
- if not url.startswith(base_url):
- output.Error('The directory URL "%s" is not relative to the '
- 'base_url: %s' % (url, base_url))
- return
-
- # Prep the default file -- it MUST be just a filename
- file = attributes.get('default_file')
- if file:
- file = encoder.MaybeNarrowPath(file)
- if os.sep in file:
- output.Error('The default_file "%s" can not include path information.'
- % file)
- file = None
-
- # Prep the remove_empty_directories -- default is false
- remove_empty_directories = attributes.get('remove_empty_directories')
- if remove_empty_directories:
- if (remove_empty_directories == '1') or \
- (remove_empty_directories.lower() == 'true'):
- remove_empty_directories = True
- elif (remove_empty_directories == '0') or \
- (remove_empty_directories.lower() == 'false'):
- remove_empty_directories = False
- # otherwise the user set a non-default value
- else:
- output.Error('Configuration file remove_empty_directories '
- 'value is not recognized. Value must be true or false.')
- return
- else:
- remove_empty_directories = False
-
- self._path = path
- self._url = url
- self._default_file = file
- self._remove_empty_directories = remove_empty_directories
-
- if file:
- output.Log('Input: From DIRECTORY "%s" (%s) with default file "%s"'
- % (path, url, file), 2)
- else:
- output.Log('Input: From DIRECTORY "%s" (%s) with no default file'
- % (path, url), 2)
- # end def __init__
-
- def ProduceURLs(self, consumer):
- """ Produces URLs from our data source, hands them in to the consumer. """
- if not self._path:
- return
-
- root_path = self._path
- root_URL = self._url
- root_file = self._default_file
- remove_empty_directories = self._remove_empty_directories
-
- def HasReadPermissions(path):
- """ Verifies a given path has read permissions. """
- stat_info = os.stat(path)
- mode = stat_info[stat.ST_MODE]
- if mode & stat.S_IREAD:
- return True
- else:
- return None
-
- def PerFile(dirpath, name):
- """
- Called once per file.
- Note that 'name' will occasionally be None -- for a directory itself
- """
- # Pull a timestamp
- url = URL()
- isdir = False
- try:
- if name:
- path = os.path.join(dirpath, name)
- else:
- path = dirpath
- isdir = os.path.isdir(path)
- time = None
- if isdir and root_file:
- file = os.path.join(path, root_file)
- try:
- time = os.stat(file)[stat.ST_MTIME]
- except OSError:
- pass
- if not time:
- time = os.stat(path)[stat.ST_MTIME]
- url.lastmod = TimestampISO8601(time)
- except OSError:
- pass
- except ValueError:
- pass
-
- # Build a URL
- middle = dirpath[len(root_path):]
- if os.sep != '/':
- middle = middle.replace(os.sep, '/')
- if middle:
- middle = middle + '/'
- if name:
- middle = middle + name
- if isdir:
- middle = middle + '/'
- url.TrySetAttribute(
- 'loc', root_URL + encoder.WidenText(middle, None))
-
- # Suppress default files. (All the way down here so we can log
- # it.)
- if name and (root_file == name):
- url.Log(prefix='IGNORED (default file)', level=2)
- return
-
- # Suppress directories when remove_empty_directories="true"
- try:
- if isdir:
- if HasReadPermissions(path):
- if remove_empty_directories == 'true' and \
- len(os.listdir(path)) == 0:
- output.Log(
- 'IGNORED empty directory %s' % str(path), level=1)
- return
- elif path == self._path:
- output.Error('IGNORED configuration file directory input %s due '
- 'to file permissions' % self._path)
- else:
- output.Log('IGNORED files within directory %s due to file '
- 'permissions' % str(path), level=0)
- except OSError:
- pass
- except ValueError:
- pass
-
- consumer(url, False)
- # end def PerFile
-
- def PerDirectory(ignore, dirpath, namelist):
- """
- Called once per directory with a list of all the contained files/dirs.
- """
- ignore = ignore # Avoid warnings of an unused parameter
-
- if not dirpath.startswith(root_path):
- output.Warn('Unable to decide what the root path is for directory: '
- '%s' % dirpath)
- return
-
- for name in namelist:
- PerFile(dirpath, name)
- # end def PerDirectory
-
- output.Log('Walking DIRECTORY "%s"' % self._path, 1)
- PerFile(self._path, None)
- os.path.walk(self._path, PerDirectory, None)
- # end def ProduceURLs
-# end class InputDirectory
-
-
-class InputAccessLog:
- """
- Each Input class knows how to yield a set of URLs from a data source.
-
- This one handles access logs. It's non-trivial in that we want to
- auto-detect log files in the Common Logfile Format (as used by Apache,
- for instance) and the Extended Log File Format (as used by IIS, for
- instance).
- """
-
- def __init__(self, attributes):
- self._path = None # The file path
- self._encoding = None # Encoding of that file
- self._is_elf = False # Extended Log File Format?
- self._is_clf = False # Common Logfile Format?
- self._elf_status = -1 # ELF field: '200'
- self._elf_method = -1 # ELF field: 'HEAD'
- self._elf_uri = -1 # ELF field: '/foo?bar=1'
- self._elf_urifrag1 = -1 # ELF field: '/foo'
- self._elf_urifrag2 = -1 # ELF field: 'bar=1'
-
- if not ValidateAttributes('ACCESSLOG', attributes, ('path', 'encoding')):
- return
-
- self._path = attributes.get('path')
- self._encoding = attributes.get('encoding', ENC_UTF8)
- if self._path:
- self._path = encoder.MaybeNarrowPath(self._path)
- if os.path.isfile(self._path):
- output.Log('Input: From ACCESSLOG "%s"' % self._path, 2)
- else:
- output.Error('Can not locate file: %s' % self._path)
- self._path = None
- else:
- output.Error('Accesslog entries must have a "path" attribute.')
- # end def __init__
-
- def RecognizeELFLine(self, line):
- """ Recognize the Fields directive that heads an ELF file """
- if not line.startswith('#Fields:'):
- return False
- fields = line.split(' ')
- del fields[0]
- for i in range(0, len(fields)):
- field = fields[i].strip()
- if field == 'sc-status':
- self._elf_status = i
- elif field == 'cs-method':
- self._elf_method = i
- elif field == 'cs-uri':
- self._elf_uri = i
- elif field == 'cs-uri-stem':
- self._elf_urifrag1 = i
- elif field == 'cs-uri-query':
- self._elf_urifrag2 = i
- output.Log('Recognized an Extended Log File Format file.', 2)
- return True
- # end def RecognizeELFLine
-
- def GetELFLine(self, line):
- """ Fetch the requested URL from an ELF line """
- fields = line.split(' ')
- count = len(fields)
-
- # Verify status was Ok
- if self._elf_status >= 0:
- if self._elf_status >= count:
- return None
- if not fields[self._elf_status].strip() == '200':
- return None
-
- # Verify method was HEAD or GET
- if self._elf_method >= 0:
- if self._elf_method >= count:
- return None
- if not fields[self._elf_method].strip() in ('HEAD', 'GET'):
- return None
-
- # Pull the full URL if we can
- if self._elf_uri >= 0:
- if self._elf_uri >= count:
- return None
- url = fields[self._elf_uri].strip()
- if url != '-':
- return url
-
- # Put together a fragmentary URL
- if self._elf_urifrag1 >= 0:
- if self._elf_urifrag1 >= count or self._elf_urifrag2 >= count:
- return None
- urlfrag1 = fields[self._elf_urifrag1].strip()
- urlfrag2 = None
- if self._elf_urifrag2 >= 0:
- urlfrag2 = fields[self._elf_urifrag2]
- if urlfrag1 and (urlfrag1 != '-'):
- if urlfrag2 and (urlfrag2 != '-'):
- urlfrag1 = urlfrag1 + '?' + urlfrag2
- return urlfrag1
-
- return None
- # end def GetELFLine
-
- def RecognizeCLFLine(self, line):
- """ Try to tokenize a logfile line according to CLF pattern and see if
- it works. """
- match = ACCESSLOG_CLF_PATTERN.match(line)
- recognize = match and (match.group(1) in ('HEAD', 'GET'))
- if recognize:
- output.Log('Recognized a Common Logfile Format file.', 2)
- return recognize
- # end def RecognizeCLFLine
-
- def GetCLFLine(self, line):
- """ Fetch the requested URL from a CLF line """
- match = ACCESSLOG_CLF_PATTERN.match(line)
- if match:
- request = match.group(1)
- if request in ('HEAD', 'GET'):
- return match.group(2)
- return None
- # end def GetCLFLine
-
- def ProduceURLs(self, consumer):
- """ Produces URLs from our data source, hands them in to the consumer. """
-
- # Open the file
- (frame, file) = OpenFileForRead(self._path, 'ACCESSLOG')
- if not file:
- return
-
- # Iterate lines
- for line in file.readlines():
- if self._encoding:
- line = encoder.WidenText(line, self._encoding)
- line = line.strip()
-
- # If we don't know the format yet, try them both
- if (not self._is_clf) and (not self._is_elf):
- self._is_elf = self.RecognizeELFLine(line)
- self._is_clf = self.RecognizeCLFLine(line)
-
- # Digest the line
- match = None
- if self._is_elf:
- match = self.GetELFLine(line)
- elif self._is_clf:
- match = self.GetCLFLine(line)
- if not match:
- continue
-
- # Pass it on
- url = URL()
- url.TrySetAttribute('loc', match)
- consumer(url, True)
-
- file.close()
- if frame:
- frame.close()
- # end def ProduceURLs
-# end class InputAccessLog
-
-
-class FilePathGenerator:
- """
- This class generates filenames in a series, upon request.
- You can request any iteration number at any time, you don't
- have to go in order.
-
- Example of iterations for '/path/foo.xml.gz':
- 0 --> /path/foo.xml.gz
- 1 --> /path/foo1.xml.gz
- 2 --> /path/foo2.xml.gz
- _index.xml --> /path/foo_index.xml
- """
-
- def __init__(self):
- self.is_gzip = False # Is this a GZIP file?
-
- self._path = None # '/path/'
- self._prefix = None # 'foo'
- self._suffix = None # '.xml.gz'
- # end def __init__
-
- def Preload(self, path):
- """ Splits up a path into forms ready for recombination. """
- path = encoder.MaybeNarrowPath(path)
-
- # Get down to a base name
- path = os.path.normpath(path)
- base = os.path.basename(path).lower()
- if not base:
- output.Error('Couldn\'t parse the file path: %s' % path)
- return False
- lenbase = len(base)
-
- # Recognize extension
- lensuffix = 0
- compare_suffix = ['.xml', '.xml.gz', '.gz']
- for suffix in compare_suffix:
- if base.endswith(suffix):
- lensuffix = len(suffix)
- break
- if not lensuffix:
- output.Error('The path "%s" doesn\'t end in a supported file '
- 'extension.' % path)
- return False
- self.is_gzip = suffix.endswith('.gz')
-
- # Split the original path
- lenpath = len(path)
- self._path = path[:lenpath - lenbase]
- self._prefix = path[lenpath - lenbase:lenpath - lensuffix]
- self._suffix = path[lenpath - lensuffix:]
-
- return True
- # end def Preload
-
- def GeneratePath(self, instance):
- """ Generates the iterations, as described above. """
- prefix = self._path + self._prefix
- if isinstance(instance, int):
- if instance:
- return '%s%d%s' % (prefix, instance, self._suffix)
- return prefix + self._suffix
- return prefix + instance
- # end def GeneratePath
-
- def GenerateURL(self, instance, root_url):
- """ Generates iterations, but as a URL instead of a path. """
- prefix = root_url + self._prefix
- retval = None
- if isinstance(instance, int):
- if instance:
- retval = '%s%d%s' % (prefix, instance, self._suffix)
- else:
- retval = prefix + self._suffix
- else:
- retval = prefix + instance
- return URL.Canonicalize(retval)
- # end def GenerateURL
-
- def GenerateWildURL(self, root_url):
- """ Generates a wildcard that should match all our iterations """
- prefix = URL.Canonicalize(root_url + self._prefix)
- temp = URL.Canonicalize(prefix + self._suffix)
- suffix = temp[len(prefix):]
- return prefix + '*' + suffix
- # end def GenerateURL
-# end class FilePathGenerator
-
-
-class PerURLStatistics:
- """ Keep track of some simple per-URL statistics, like file extension. """
-
- def __init__(self):
- self._extensions = {} # Count of extension instances
- # end def __init__
-
- def Consume(self, url):
- """ Log some stats for the URL. At the moment, that means extension. """
- if url and url.loc:
- (scheme, netloc, path, query, frag) = urlsplit(url.loc)
- if not path:
- return
-
- # Recognize directories
- if path.endswith('/'):
- if '/' in self._extensions:
- self._extensions['/'] = self._extensions['/'] + 1
- else:
- self._extensions['/'] = 1
- return
-
- # Strip to a filename
- i = path.rfind('/')
- if i >= 0:
- assert i < len(path)
- path = path[i:]
-
- # Find extension
- i = path.rfind('.')
- if i > 0:
- assert i < len(path)
- ext = path[i:].lower()
- if ext in self._extensions:
- self._extensions[ext] = self._extensions[ext] + 1
- else:
- self._extensions[ext] = 1
- else:
- if '(no extension)' in self._extensions:
- self._extensions['(no extension)'] = self._extensions[
- '(no extension)'] + 1
- else:
- self._extensions['(no extension)'] = 1
- # end def Consume
-
- def Log(self):
- """ Dump out stats to the output. """
- if len(self._extensions):
- output.Log('Count of file extensions on URLs:', 1)
- set = sorted(self._extensions.keys())
- for ext in set:
- output.Log(' %7d %s' % (self._extensions[ext], ext), 1)
- # end def Log
-
-
-class Sitemap(xml.sax.handler.ContentHandler):
- """
- This is the big workhorse class that processes your inputs and spits
- out sitemap files. It is built as a SAX handler for set up purposes.
- That is, it processes an XML stream to bring itself up.
- """
-
- def __init__(self, suppress_notify):
- xml.sax.handler.ContentHandler.__init__(self)
- self._filters = [] # Filter objects
- self._inputs = [] # Input objects
- self._urls = {} # Maps URLs to count of dups
- self._set = [] # Current set of URLs
- self._filegen = None # Path generator for output files
- self._wildurl1 = None # Sitemap URLs to filter out
- self._wildurl2 = None # Sitemap URLs to filter out
- self._sitemaps = 0 # Number of output files
- # We init _dup_max to 2 so the default priority is 0.5 instead of 1.0
- self._dup_max = 2 # Max number of duplicate URLs
- self._stat = PerURLStatistics() # Some simple stats
- self._in_site = False # SAX: are we in a Site node?
- self._in_Site_ever = False # SAX: were we ever in a Site?
-
- self._default_enc = None # Best encoding to try on URLs
- self._base_url = None # Prefix to all valid URLs
- self._store_into = None # Output filepath
- self._sitemap_type = None # Sitemap type (web, mobile or news)
- self._suppress = suppress_notify # Suppress notify of servers
- # end def __init__
-
- def ValidateBasicConfig(self):
- """ Verifies (and cleans up) the basic user-configurable options. """
- all_good = True
-
- if self._default_enc:
- encoder.SetUserEncoding(self._default_enc)
-
- # Canonicalize the base_url
- if all_good and not self._base_url:
- output.Error('A site needs a "base_url" attribute.')
- all_good = False
- if all_good and not URL.IsAbsolute(self._base_url):
- output.Error('The "base_url" must be absolute, not relative: %s' %
- self._base_url)
- all_good = False
- if all_good:
- self._base_url = URL.Canonicalize(self._base_url)
- if not self._base_url.endswith('/'):
- self._base_url = self._base_url + '/'
- output.Log('BaseURL is set to: %s' % self._base_url, 2)
-
- # Load store_into into a generator
- if all_good:
- if self._store_into:
- self._filegen = FilePathGenerator()
- if not self._filegen.Preload(self._store_into):
- all_good = False
- else:
- output.Error('A site needs a "store_into" attribute.')
- all_good = False
-
- # Ask the generator for patterns on what its output will look like
- if all_good:
- self._wildurl1 = self._filegen.GenerateWildURL(self._base_url)
- self._wildurl2 = self._filegen.GenerateURL(SITEINDEX_SUFFIX,
- self._base_url)
-
- # Unify various forms of False
- if all_good:
- if self._suppress:
- if (isinstance(self._suppress, bytes_str)) or (isinstance(self._suppress, unicode_str)):
- if (self._suppress == '0') or (self._suppress.lower() == 'false'):
- self._suppress = False
-
- # Clean up the sitemap_type
- if all_good:
- match = False
- # If sitemap_type is not specified, default to web sitemap
- if not self._sitemap_type:
- self._sitemap_type = 'web'
- else:
- self._sitemap_type = self._sitemap_type.lower()
- for pattern in SITEMAP_TYPES:
- if self._sitemap_type == pattern:
- match = True
- break
- if not match:
- output.Error('The "sitemap_type" value must be "web", "mobile" '
- 'or "news": %s' % self._sitemap_type)
- all_good = False
- output.Log('The Sitemap type is %s Sitemap.' %
- self._sitemap_type.upper(), 0)
-
- # Done
- if not all_good:
- output.Log('See "example_config.xml" for more information.', 0)
- return all_good
- # end def ValidateBasicConfig
-
- def Generate(self):
- """ Run over all the Inputs and ask them to Produce """
- # Run the inputs
- for input in self._inputs:
- input.ProduceURLs(self.ConsumeURL)
-
- # Do last flushes
- if len(self._set):
- self.FlushSet()
- if not self._sitemaps:
- output.Warn('No URLs were recorded, writing an empty sitemap.')
- self.FlushSet()
-
- # Write an index as needed
- if self._sitemaps > 1:
- self.WriteIndex()
-
- # Notify
- self.NotifySearch()
-
- # Dump stats
- self._stat.Log()
- # end def Generate
-
- def ConsumeURL(self, url, allow_fragment):
- """
- All per-URL processing comes together here, regardless of Input.
- Here we run filters, remove duplicates, spill to disk as needed, etc.
-
- """
- if not url:
- return
-
- # Validate
- if not url.Validate(self._base_url, allow_fragment):
- return
-
- # Run filters
- accept = None
- for filter in self._filters:
- accept = filter.Apply(url)
- if accept is not None:
- break
- if not (accept or (accept is None)):
- url.Log(prefix='FILTERED', level=2)
- return
-
- # Ignore our out output URLs
- if fnmatch.fnmatchcase(url.loc, self._wildurl1) or fnmatch.fnmatchcase(
- url.loc, self._wildurl2):
- url.Log(prefix='IGNORED (output file)', level=2)
- return
-
- # Note the sighting
- hash = url.MakeHash()
- if hash in self._urls:
- dup = self._urls[hash]
- if dup > 0:
- dup = dup + 1
- self._urls[hash] = dup
- if self._dup_max < dup:
- self._dup_max = dup
- url.Log(prefix='DUPLICATE')
- return
-
- # Acceptance -- add to set
- self._urls[hash] = 1
- self._set.append(url)
- self._stat.Consume(url)
- url.Log()
-
- # Flush the set if needed
- if len(self._set) >= MAXURLS_PER_SITEMAP:
- self.FlushSet()
- # end def ConsumeURL
-
- def FlushSet(self):
- """
- Flush the current set of URLs to the output. This is a little
- slow because we like to sort them all and normalize the priorities
- before dumping.
- """
-
- # Determine what Sitemap header to use (News or General)
- if self._sitemap_type == 'news':
- sitemap_header = NEWS_SITEMAP_HEADER
- else:
- sitemap_header = GENERAL_SITEMAP_HEADER
-
- # Sort and normalize
- output.Log('Sorting and normalizing collected URLs.', 1)
- self._set.sort()
- for url in self._set:
- hash = url.MakeHash()
- dup = self._urls[hash]
- if dup > 0:
- self._urls[hash] = -1
- if not url.priority:
- url.priority = '%.4f' % (float(dup) / float(self._dup_max))
-
- # Get the filename we're going to write to
- filename = self._filegen.GeneratePath(self._sitemaps)
- if not filename:
- output.Fatal('Unexpected: Couldn\'t generate output filename.')
- self._sitemaps = self._sitemaps + 1
- output.Log('Writing Sitemap file "%s" with %d URLs' %
- (filename, len(self._set)), 1)
-
- # Write to it
- frame = None
- file = None
-
- try:
- if self._filegen.is_gzip:
- basename = os.path.basename(filename)
- frame = open(filename, 'wb')
- file = gzip.GzipFile(
- fileobj=frame, filename=basename, mode='wt')
- else:
- file = open(filename, 'wt')
-
- file.write(sitemap_header)
- for url in self._set:
- url.WriteXML(file)
- file.write(SITEMAP_FOOTER)
-
- file.close()
- if frame:
- frame.close()
-
- frame = None
- file = None
- except IOError:
- output.Fatal('Couldn\'t write out to file: %s' % filename)
- os.chmod(filename, 0o0644)
-
- # Flush
- self._set = []
- # end def FlushSet
-
- def WriteIndex(self):
- """ Write the master index of all Sitemap files """
- # Make a filename
- filename = self._filegen.GeneratePath(SITEINDEX_SUFFIX)
- if not filename:
- output.Fatal(
- 'Unexpected: Couldn\'t generate output index filename.')
- output.Log('Writing index file "%s" with %d Sitemaps' %
- (filename, self._sitemaps), 1)
-
- # Determine what Sitemap index header to use (News or General)
- if self._sitemap_type == 'news':
- sitemap_index_header = NEWS_SITEMAP_HEADER
- else:
- sitemap_index_header = GENERAL_SITEMAP_HEADER
-
- # Make a lastmod time
- lastmod = TimestampISO8601(time.time())
-
- # Write to it
- try:
- fd = open(filename, 'wt')
- fd.write(sitemap_index_header)
-
- for mapnumber in range(0, self._sitemaps):
- # Write the entry
- mapurl = self._filegen.GenerateURL(mapnumber, self._base_url)
- mapattributes = {'loc': mapurl, 'lastmod': lastmod}
- fd.write(SITEINDEX_ENTRY % mapattributes)
-
- fd.write(SITEINDEX_FOOTER)
-
- fd.close()
- fd = None
- except IOError:
- output.Fatal('Couldn\'t write out to file: %s' % filename)
- os.chmod(filename, 0o0644)
- # end def WriteIndex
-
- def NotifySearch(self):
- """ Send notification of the new Sitemap(s) to the search engines. """
- if self._suppress:
- output.Log('Search engine notification is suppressed.', 1)
- return
-
- output.Log('Notifying search engines.', 1)
-
- # Override the urllib's opener class with one that doesn't ignore 404s
- class ExceptionURLopener(FancyURLopener):
- def http_error_default(self, url, fp, errcode, errmsg, headers):
- output.Log('HTTP error %d: %s' % (errcode, errmsg), 2)
- raise IOError
- # end def http_error_default
- # end class ExceptionURLOpener
- if sys.version_info[0] == 3:
- old_opener = urllib.request._urlopener
- urllib.request._urlopener = ExceptionURLopener()
- else:
- old_opener = urllib._urlopener
- urllib._urlopener = ExceptionURLopener()
-
- # Build the URL we want to send in
- if self._sitemaps > 1:
- url = self._filegen.GenerateURL(SITEINDEX_SUFFIX, self._base_url)
- else:
- url = self._filegen.GenerateURL(0, self._base_url)
-
- # Test if we can hit it ourselves
- try:
- u = urlopen(url)
- u.close()
- except IOError:
- output.Error('When attempting to access our generated Sitemap at the '
- 'following URL:\n %s\n we failed to read it. Please '
- 'verify the store_into path you specified in\n'
- ' your configuration file is web-accessable. Consult '
- 'the FAQ for more\n information.' % url)
- output.Warn('Proceeding to notify with an unverifyable URL.')
-
- # Cycle through notifications
- # To understand this, see the comment near the NOTIFICATION_SITES
- # comment
- for ping in NOTIFICATION_SITES:
- query_map = ping[3]
- query_attr = ping[5]
- query_map[query_attr] = url
- query = urllib.urlencode(query_map)
- notify = urlunsplit((ping[0], ping[1], ping[2], query, ping[4]))
-
- # Send the notification
- output.Log('Notifying: %s' % ping[1], 0)
- output.Log('Notification URL: %s' % notify, 2)
- try:
- u = urlopen(notify)
- u.read()
- u.close()
- except IOError:
- output.Warn('Cannot contact: %s' % ping[1])
-
- if old_opener:
- if sys.version_info[0] == 3:
- urllib.request._urlopener = old_opener
- else:
- urllib._urlopener = old_opener
- # end def NotifySearch
-
- def startElement(self, tag, attributes):
- """ SAX processing, called per node in the config stream. """
- if tag == 'site':
- if self._in_site:
- output.Error('Can not nest Site entries in the configuration.')
- else:
- self._in_site = True
-
- if not ValidateAttributes('SITE', attributes,
- ('verbose', 'default_encoding', 'base_url', 'store_into',
- 'suppress_search_engine_notify', 'sitemap_type')):
- return
-
- verbose = attributes.get('verbose', 0)
- if verbose:
- output.SetVerbose(verbose)
-
- self._default_enc = attributes.get('default_encoding')
- self._base_url = attributes.get('base_url')
- self._store_into = attributes.get('store_into')
- self._sitemap_type = attributes.get('sitemap_type')
- if not self._suppress:
- self._suppress = attributes.get(
- 'suppress_search_engine_notify',
- False)
- self.ValidateBasicConfig()
- elif tag == 'filter':
- self._filters.append(Filter(attributes))
-
- elif tag == 'url':
- print(type(attributes))
- self._inputs.append(InputURL(attributes))
-
- elif tag == 'urllist':
- for attributeset in ExpandPathAttribute(attributes, 'path'):
- if self._sitemap_type == 'news':
- self._inputs.append(InputNewsURLList(attributeset))
- else:
- self._inputs.append(InputURLList(attributeset))
-
- elif tag == 'directory':
- self._inputs.append(InputDirectory(attributes, self._base_url))
-
- elif tag == 'accesslog':
- for attributeset in ExpandPathAttribute(attributes, 'path'):
- self._inputs.append(InputAccessLog(attributeset))
- else:
- output.Error('Unrecognized tag in the configuration: %s' % tag)
- # end def startElement
-
- def endElement(self, tag):
- """ SAX processing, called per node in the config stream. """
- if tag == 'site':
- assert self._in_site
- self._in_site = False
- self._in_site_ever = True
- # end def endElement
-
- def endDocument(self):
- """ End of SAX, verify we can proceed. """
- if not self._in_site_ever:
- output.Error('The configuration must specify a "site" element.')
- else:
- if not self._inputs:
- output.Warn('There were no inputs to generate a sitemap from.')
- # end def endDocument
-# end class Sitemap
-
-
-def ValidateAttributes(tag, attributes, goodattributes):
- """ Makes sure 'attributes' does not contain any attribute not
- listed in 'goodattributes' """
- all_good = True
- for attr in attributes.keys():
- if not attr in goodattributes:
- output.Error('Unknown %s attribute: %s' % (tag, attr))
- all_good = False
- return all_good
-# end def ValidateAttributes
-
-
-def ExpandPathAttribute(src, attrib):
- """ Given a dictionary of attributes, return a list of dictionaries
- with all the same attributes except for the one named attrib.
- That one, we treat as a file path and expand into all its possible
- variations. """
- # Do the path expansion. On any error, just return the source dictionary.
- path = src.get(attrib)
- if not path:
- return [src]
- path = encoder.MaybeNarrowPath(path)
- pathlist = glob.glob(path)
- if not pathlist:
- return [src]
-
- # If this isn't actually a dictionary, make it one
- if not isinstance(src, dict):
- tmp = {}
- for key in src.keys():
- tmp[key] = src[key]
- src = tmp
- # Create N new dictionaries
- retval = []
- for path in pathlist:
- dst = src.copy()
- dst[attrib] = path
- retval.append(dst)
-
- return retval
-# end def ExpandPathAttribute
-
-
-def OpenFileForRead(path, logtext):
- """ Opens a text file, be it GZip or plain """
-
- frame = None
- file = None
-
- if not path:
- return (frame, file)
-
- try:
- if path.endswith('.gz'):
- frame = open(path, 'rb')
- file = gzip.GzipFile(fileobj=frame, mode='rt')
- else:
- file = open(path, 'rt')
-
- if logtext:
- output.Log('Opened %s file: %s' % (logtext, path), 1)
- else:
- output.Log('Opened file: %s' % path, 1)
- except IOError:
- output.Error('Can not open file: %s' % path)
-
- return (frame, file)
-# end def OpenFileForRead
-
-
-def TimestampISO8601(t):
- """Seconds since epoch (1970-01-01) --> ISO 8601 time string."""
- return time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(t))
-# end def TimestampISO8601
-
-
-def CreateSitemapFromFile(configpath, suppress_notify):
- """ Sets up a new Sitemap object from the specified configuration file. """
-
- # Remember error count on the way in
- num_errors = output.num_errors
-
- # Rev up SAX to parse the config
- sitemap = Sitemap(suppress_notify)
- try:
- output.Log('Reading configuration file: %s' % configpath, 0)
- xml.sax.parse(configpath, sitemap)
- except IOError:
- output.Error('Cannot read configuration file: %s' % configpath)
- except xml.sax._exceptions.SAXParseException as e:
- output.Error('XML error in the config file (line %d, column %d): %s' %
- (e._linenum, e._colnum, e.getMessage()))
- except xml.sax._exceptions.SAXReaderNotAvailable:
- output.Error('Some installs of Python 2.2 did not include complete support'
- ' for XML.\n Please try upgrading your version of Python'
- ' and re-running the script.')
-
- # If we added any errors, return no sitemap
- if num_errors == output.num_errors:
- return sitemap
- return None
-# end def CreateSitemapFromFile
-
-
-def ProcessCommandFlags(args):
- """
- Parse command line flags per specified usage, pick off key, value pairs
- All flags of type "--key=value" will be processed as __flags[key] = value,
- "--option" will be processed as __flags[option] = option
- """
-
- flags = {}
- rkeyval = '--(?P<key>\S*)[=](?P<value>\S*)' # --key=val
- roption = '--(?P<option>\S*)' # --key
- r = '(' + rkeyval + ')|(' + roption + ')'
- rc = re.compile(r)
- for a in args:
- try:
- rcg = rc.search(a).groupdict()
- if 'key' in rcg:
- flags[rcg['key']] = rcg['value']
- if 'option' in rcg:
- flags[rcg['option']] = rcg['option']
- except AttributeError:
- return None
- return flags
-# end def ProcessCommandFlags
-
-
-#
-# __main__
-#
-
-if __name__ == '__main__':
- flags = ProcessCommandFlags(sys.argv[1:])
- if not flags or not 'config' in flags or 'help' in flags:
- output.Log(__usage__, 0)
- else:
- suppress_notify = 'testing' in flags
- sitemap = CreateSitemapFromFile(flags['config'], suppress_notify)
- if not sitemap:
- output.Log('Configuration file errors -- exiting.', 0)
- else:
- sitemap.Generate()
- output.Log('Number of errors: %d' % output.num_errors, 1)
- output.Log('Number of warnings: %d' % output.num_warns, 1)