Skip to content
Browse files

Merge pull request #16 from rizumu/biblia

multi-markup, internationalized models, post_published signal
  • Loading branch information...
2 parents 386f45f + 5eee15b commit bc64605cb0edb2202a24beb101bc555fc44f7313 @paltman paltman committed
View
2 README.rst
@@ -11,7 +11,7 @@ internal Pinax blog app once we've made it feature complete.
Current features include:
* support for multiple channels (e.g. technical vs business)
- * use of Creole as markup format
+ * use HTML, Creole, Markdown, reStructuredText or Textile as markup formats
* Atom feeds
* previewing of blog posts before publishing
* optional ability to announce new posts on twitter
View
6 biblion/admin.py
@@ -4,7 +4,7 @@
from biblion.models import Biblion, Post, Image
from biblion.forms import AdminPostForm
-from biblion.utils import can_tweet
+from biblion.utils.twitter import can_tweet
class ImageInline(admin.TabularInline):
@@ -18,7 +18,7 @@ class BiblionAdmin(admin.ModelAdmin):
}
-class PostAdmin(admin.ModelAdmin):
+class PostAdmin(admin.ModelAdmin):
list_display = ["biblion", "title", "published_flag"]
list_filter = ["biblion"]
form = AdminPostForm
@@ -27,10 +27,12 @@ class PostAdmin(admin.ModelAdmin):
"title",
"slug",
"author",
+ "markup_type",
"teaser",
"content",
"publish",
"publish_date",
+ "sites",
]
if can_tweet():
fields.append("tweet")
View
40 biblion/forms.py
@@ -3,14 +3,14 @@
from django import forms
from django.core.exceptions import ImproperlyConfigured
-from django.utils.functional import curry
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from biblion.models import Biblion, Post, Revision, Image
-from biblion.settings import PARSER
-from biblion.utils import can_tweet, load_path_attr, slugify_unique
+from biblion.signals import post_published
+from biblion.utils.twitter import can_tweet
+from biblion.utils.slugify import slugify_unique
class BiblionForm(forms.ModelForm):
@@ -131,14 +131,12 @@ def save(self):
if Post.objects.filter(pk=post.pk, published=None).count():
if self.cleaned_data["publish"]:
post.published = datetime.datetime.now()
-
+
if self.cleaned_data["publish_date"]:
post.published = self.cleaned_data["publish_date"]
-
- render_func = curry(load_path_attr(PARSER[0]), **PARSER[1])
- post.teaser_html = render_func(self.cleaned_data["teaser"])
- post.content_html = render_func(self.cleaned_data["content"])
+ post.teaser = self.cleaned_data["teaser"]
+ post.content = self.cleaned_data["content"]
post.updated = datetime.datetime.now()
post.save()
@@ -211,6 +209,7 @@ def __init__(self, *args, **kwargs):
if latest_revision:
# set initial data from the latest revision
+ self.fields["markup_type"].initial = latest_revision.markup_type
self.fields["teaser"].initial = latest_revision.teaser
self.fields["content"].initial = latest_revision.content
@@ -221,28 +220,29 @@ def __init__(self, *args, **kwargs):
def save(self):
post = super(AdminPostForm, self).save(commit=False)
-
- if post.pk is None:
- if self.cleaned_data["publish"]:
- post.published = datetime.datetime.now()
+ # only publish the first time publish has been checked
+ if (post.pk is None or Post.objects.filter(pk=post.pk, published=None).count()) and self.cleaned_data["publish"]:
+ post.published = datetime.datetime.now()
+ send_published_signal = True
else:
- if Post.objects.filter(pk=post.pk, published=None).count():
- if self.cleaned_data["publish"]:
- post.published = datetime.datetime.now()
-
+ send_published_signal = False
+
if self.cleaned_data["publish_date"]:
post.published = self.cleaned_data["publish_date"]
-
- render_func = curry(load_path_attr(PARSER[0]), **PARSER[1])
- post.teaser_html = render_func(self.cleaned_data["teaser"])
- post.content_html = render_func(self.cleaned_data["content"])
+ post.markup_type = self.cleaned_data["markup_type"]
+ post.teaser = self.cleaned_data["teaser"]
+ post.content = self.cleaned_data["content"]
post.updated = datetime.datetime.now()
post.save()
+ if send_published_signal:
+ post_published.send(sender=self, pk=post.pk)
+
r = Revision()
r.post = post
r.title = post.title
+ r.markup_type = self.cleaned_data["markup_type"]
r.teaser = self.cleaned_data["teaser"]
r.content = self.cleaned_data["content"]
r.author = post.author
View
5 biblion/markdown_parser.py
@@ -1,5 +0,0 @@
-import markdown
-
-
-def parse(text):
- return markdown.markdown(text)
View
119 biblion/models.py
@@ -1,4 +1,4 @@
-# -*- coding: utf8 -*-
+# -*- coding: utf-8 -*-
import urllib2
from datetime import datetime
@@ -7,7 +7,9 @@
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.core.urlresolvers import reverse
+from django.utils import formats
from django.utils import simplejson as json
+from django.utils.translation import ugettext as _
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
@@ -18,17 +20,21 @@
twitter = None
from biblion.managers import PostManager
-from biblion.utils import can_tweet
+from biblion.utils.twitter import can_tweet
class Biblion(models.Model):
- title = models.CharField(max_length=128)
- subtitle = models.CharField(max_length=256, null=True, blank=True)
- slug = models.SlugField(unique=True)
- description = models.TextField()
- logo = models.FileField(upload_to="biblion_biblion_logo")
- sites = models.ManyToManyField(Site)
+ title = models.CharField(_("title"), max_length=128)
+ subtitle = models.CharField(_("subtitle"), max_length=256, blank=True)
+ slug = models.SlugField(_("slug"), unique=True)
+ description = models.TextField(_("description"))
+ logo = models.FileField(_("logo"), upload_to="biblion_biblion_logo")
+ sites = models.ManyToManyField(Site, verbose_name=_("list of sites"))
+
+ class Meta:
+ verbose_name = _("biblion")
+ verbose_name_plural = _("biblia")
def __unicode__(self):
return unicode(self.title)
@@ -39,30 +45,44 @@ def get_absolute_url(self):
class BiblionContributor(models.Model):
- biblion = models.ForeignKey(Biblion)
- user = models.ForeignKey(User)
- role = models.CharField(max_length=25)
+ biblion = models.ForeignKey(Biblion, verbose_name=_("biblion"))
+ user = models.ForeignKey(User, related_name="contributors", verbose_name=_("user"))
+ role = models.CharField(_("role"), max_length=25)
+
+ class Meta:
+ verbose_name = _("biblion contributor")
+ verbose_name_plural = _("biblion contributors")
class Post(models.Model):
- biblion = models.ForeignKey(Biblion, related_name="posts")
- sites = models.ManyToManyField(Site)
+ biblion = models.ForeignKey(Biblion, related_name="posts", verbose_name=_("biblion"))
+ sites = models.ManyToManyField(Site, verbose_name=_("list of sites"))
+
+ title = models.CharField(_("title"), max_length=90)
+ slug = models.SlugField(_("slug"))
+ author = models.ForeignKey(User, related_name="posts", verbose_name=_("author"))
- title = models.CharField(max_length=90)
- slug = models.SlugField()
- author = models.ForeignKey(User, related_name="posts")
+ markup_types = [
+ _("HTML"),
+ _("Creole"),
+ _("Markdown"),
+ _("reStructuredText"),
+ _("Textile"),
+ ]
+ MARKUP_CHOICES = zip(range(1, 1 + len(markup_types)), markup_types)
+ markup_type = models.IntegerField(_("markup type"), choices=MARKUP_CHOICES, default=1)
- teaser_html = models.TextField(editable=False)
- content_html = models.TextField(editable=False)
+ teaser = models.TextField(_("teaser"), editable=False)
+ content = models.TextField(_("content"), editable=False)
- tweet_text = models.CharField(max_length=140, editable=False)
+ tweet_text = models.CharField(_("tweet text"), max_length=140, editable=False)
- created = models.DateTimeField(default=datetime.now, editable=False) # when first revision was created
- updated = models.DateTimeField(null=True, blank=True, editable=False) # when last revision was create (even if not published)
- published = models.DateTimeField(null=True, blank=True, editable=False) # when last published
+ created = models.DateTimeField(_("created"), default=datetime.now, editable=False) # when first revision was created
+ updated = models.DateTimeField(_("updated"), null=True, blank=True, editable=False) # when last revision was created (even if not published)
+ published = models.DateTimeField(_("published"), null=True, blank=True, editable=False) # when last published
- view_count = models.IntegerField(default=0, editable=False)
+ view_count = models.IntegerField(_("view count"), default=0, editable=False)
def rev(self, rev_id):
return self.revisions.get(pk=rev_id)
@@ -79,6 +99,8 @@ def latest(self):
return None
class Meta:
+ verbose_name = _("post")
+ verbose_name_plural = _("posts")
ordering = ("-published",)
get_latest_by = "published"
@@ -107,13 +129,13 @@ def as_tweet(self):
def tweet(self):
if can_tweet():
account = twitter.Api(
- username = settings.TWITTER_USERNAME,
- password = settings.TWITTER_PASSWORD,
+ username=settings.TWITTER_USERNAME,
+ password=settings.TWITTER_PASSWORD,
)
account.PostUpdate(self.as_tweet())
else:
- raise ImproperlyConfigured("Unable to send tweet due to either "
- "missing python-twitter or required settings.")
+ raise ImproperlyConfigured(_("Unable to send tweet due to either \
+ missing python-twitter or required settings."))
def save(self, **kwargs):
self.updated_at = datetime.now()
@@ -147,22 +169,29 @@ def inc_views(self):
class Revision(models.Model):
- post = models.ForeignKey(Post, related_name="revisions")
+ post = models.ForeignKey(Post, related_name="revisions", verbose_name=_("post"))
- title = models.CharField(max_length=90)
- teaser = models.TextField()
+ title = models.CharField(_("title"), max_length=90)
- content = models.TextField()
+ markup_type = models.IntegerField(_("markup type"))
+ teaser = models.TextField(_("teaser"))
+ content = models.TextField(_("content"))
- author = models.ForeignKey(User, related_name="post_revisions")
+ author = models.ForeignKey(User, related_name="post revisions", verbose_name=_("author"))
- updated = models.DateTimeField(default=datetime.now)
- published = models.DateTimeField(null=True, blank=True)
+ updated = models.DateTimeField(_("updated"), default=datetime.now)
+ published = models.DateTimeField(_("published"), null=True, blank=True)
- view_count = models.IntegerField(default=0, editable=False)
+ view_count = models.IntegerField(_("view count"), default=0, editable=False)
+
+ class Meta:
+ verbose_name = _("revision")
+ verbose_name_plural = _("revisions")
def __unicode__(self):
- return 'Revision %s for %s' % (self.updated.strftime('%Y%m%d-%H%M'), self.post.slug)
+ return _("Revision %(datetime)s for %(slug)s") % {
+ "datetime": formats.localize("%Y%m%d-%H%M"),
+ "slug": self.post.slug}
def inc_views(self):
self.view_count += 1
@@ -173,19 +202,23 @@ class Image(models.Model):
post = models.ForeignKey(Post, related_name="images", blank=True, null=True)
- image_path = models.ImageField(upload_to="images/%Y/%m/%d")
- url = models.CharField(max_length=150, blank=True)
+ image_path = models.ImageField(_("image path"), upload_to="images/%Y/%m/%d")
+ url = models.CharField(_("url"), max_length=150, blank=True)
- timestamp = models.DateTimeField(default=datetime.now, editable=False)
+ timestamp = models.DateTimeField(_("timestamp"), default=datetime.now, editable=False)
+
+ class Meta:
+ verbose_name = _("image")
+ verbose_name_plural = _("images")
def __unicode__(self):
if self.pk is not None:
- return "{{ %d }}" % self.pk
+ return u"{{ %d }}" % self.pk
else:
- return "deleted image"
+ return _("deleted image")
class FeedHit(models.Model):
- request_data = models.TextField()
- created = models.DateTimeField(default=datetime.now)
+ request_data = models.TextField(_("request data"))
+ created = models.DateTimeField(_("created"), default=datetime.now)
View
4 biblion/settings.py
@@ -1,4 +0,0 @@
-from django.conf import settings
-
-
-PARSER = getattr(settings, "BIBLION_PARSER", ["biblion.creole_parser.parse", {}])
View
4 biblion/signals.py
@@ -0,0 +1,4 @@
+from django.dispatch import Signal
+
+
+post_published = Signal(providing_args=["pk"])
View
11 biblion/templates/biblion/atom_entry.xml
@@ -1,3 +1,6 @@
+{% load biblion_tags %}
+
+
<entry xml:base="http://{{ current_site.domain }}/">
<id>http://{{ current_site.domain }}{{ entry.get_absolute_url }}</id>
<title>{{ entry.title }}</title>
@@ -12,14 +15,14 @@
<summary type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
- {{ entry.teaser_html|safe }}
+ {{ entry.teaser|to_html }}
</div>
</summary>
<content type="xhtml" xml:lang="en">
<div xmlns="http://www.w3.org/1999/xhtml">
- {{ entry.teaser_html|safe }}
- {{ entry.content_html|safe }}
+ {{ entry.teaser|to_html }}
+ {{ entry.content|to_html }}
</div>
</content>
-</entry>
+</entry>
View
0 biblion/templatetags/__init__.py
No changes.
View
19 biblion/templatetags/biblion_tags.py
@@ -0,0 +1,19 @@
+from django import template
+
+from biblion.utils.code_hilite import to_html
+
+
+register = template.Library()
+
+
+register.filter("to_html", to_html)
+
+
+def show_post_brief(context, post):
+ return {
+ "post": post,
+ "last": context["forloop"]["last"],
+ "can_edit": context["user"].is_staff,
+ }
+
+register.inclusion_tag("blog/post_brief.html", takes_context=True)(show_post_brief)
View
45 biblion/utils.py
@@ -1,45 +0,0 @@
-from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
-from django.template.defaultfilters import slugify
-try:
- from django.utils.importlib import import_module
-except ImportError:
- from importlib import import_module
-
-try:
- import twitter
-except ImportError:
- twitter = None
-
-
-def can_tweet():
- creds_available = (
- hasattr(settings, "TWITTER_USERNAME") and hasattr(settings, "TWITTER_PASSWORD")
- )
- return twitter and creds_available
-
-
-def slugify_unique(value, model, slugfield="slug"):
- suffix = 0
- potential = base = slugify(value)
- while True:
- if suffix:
- potential = "-".join([base, str(suffix)])
- if not model.objects.filter(**{slugfield: potential}).count():
- print model.objects.filter(**{slugfield: potential})
- return potential
- suffix += 1
-
-
-def load_path_attr(path):
- i = path.rfind(".")
- module, attr = path[:i], path[i+1:]
- try:
- mod = import_module(module)
- except ImportError, e:
- raise ImproperlyConfigured("Error importing %s: '%s'" % (module, e))
- try:
- attr = getattr(mod, attr)
- except AttributeError:
- raise ImproperlyConfigured("Module '%s' does not define a '%s'" % (module, attr))
- return attr
View
0 biblion/utils/__init__.py
No changes.
View
97 biblion/utils/code_hilite.py
@@ -0,0 +1,97 @@
+from pygments import highlight
+from pygments.formatters import HtmlFormatter
+from pygments.lexers import get_lexer_by_name, TextLexer
+
+from docutils import nodes
+from docutils.writers import html4css1
+from docutils.core import publish_parts
+from docutils.parsers.rst import directives
+
+import markdown
+import textile
+
+from django.utils.safestring import mark_safe
+
+from biblion.utils.mdx_codehilite import makeExtension
+from biblion.utils import creole_parser
+
+
+VARIANTS = {}
+
+
+def pygments_directive(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ try:
+ lexer = get_lexer_by_name(arguments[0])
+ except (ValueError, IndexError):
+ # no lexer found - use the text one instead of an exception
+ lexer = TextLexer()
+ parsed = highlight(u"\n".join(content), lexer, HtmlFormatter())
+ return [nodes.raw("", parsed, format="html")]
+pygments_directive.arguments = (0, 1, False)
+pygments_directive.content = 1
+pygments_directive.options = dict([(key, directives.flag) for key in VARIANTS])
+
+directives.register_directive("sourcecode", pygments_directive)
+
+
+class HTMLWriter(html4css1.Writer):
+ def __init__(self):
+ html4css1.Writer.__init__(self)
+ self.translator_class = HTMLTranslator
+
+
+class HTMLTranslator(html4css1.HTMLTranslator):
+ named_tags = []
+
+ def visit_literal(self, node):
+ # @@@ wrapping fixes.
+ self.body.append("<code>%s</code>" % node.astext())
+ raise nodes.SkipNode
+
+
+def rst_to_html(value):
+ parts = publish_parts(source=value, writer=HTMLWriter(),
+ settings_overrides={"initial_header_level": 2})
+ return parts["fragment"]
+
+
+def markdown_to_html(text):
+ """
+ Convert markdown to HTML with code hiliting
+ """
+ return unicode(markdown.markdown(text, extensions=('codehilite',)))
+
+
+def textile_to_html(text):
+ """
+ Convert textile to HTML
+ @@@ add code hiliting support
+ """
+ return unicode(textile.textile(text))
+
+
+def creole_to_html(text):
+ """
+ Convert creole to HTML
+ @@@ add code hiliting support
+ """
+ return unicode(creole_parser.parse(text, emitter=creole_parser.BiblionHtmlEmitter))
+
+
+def to_html(obj):
+ """
+ Markup filter that converts an object to html formatting.
+ Syntax hiliting support for rst and markdown only.
+ """
+ if obj.markup_type == 1: # HTML
+ html = obj.content
+ elif obj.markup_type == 2: # Creole
+ html = creole_to_html(obj.content)
+ elif obj.markup_type == 3: # Markdown
+ html = markdown_to_html(obj.content)
+ elif obj.markup_type == 4: # reStructuredText
+ html = rst_to_html(obj.content)
+ elif obj.markup_type == 5: # Textile
+ html = textile_to_html(obj.content)
+ return mark_safe(html)
View
55 biblion/creole_parser.py → biblion/utils/creole_parser.py
@@ -11,12 +11,13 @@
class Rules:
# For the link targets:
- proto = r'http|https|ftp|nntp|news|mailto|telnet|file|irc'
- extern = r'(?P<extern_addr>(?P<extern_proto>%s):.*)' % proto
- interwiki = r'''
+ proto = r"http|https|ftp|nntp|news|mailto|telnet|file|irc"
+ extern = r"(?P<extern_addr>(?P<extern_proto>%s):.*)" % proto
+ interwiki = r"""
(?P<inter_wiki> [A-Z][a-zA-Z]+ ) :
(?P<inter_page> .* )
- '''
+ """
+
class HtmlEmitter(object):
"""
@@ -24,10 +25,10 @@ class HtmlEmitter(object):
tree consisting of DocNodes.
"""
- addr_re = re.compile('|'.join([
+ addr_re = re.compile("|".join([
Rules.extern,
Rules.interwiki,
- ]), re.X | re.U) # for addresses
+ ]), re.X | re.U) # for addresses
def __init__(self, root):
self.root = root
@@ -35,15 +36,15 @@ def __init__(self, root):
def get_text(self, node):
"""Try to emit whatever text is in the node."""
try:
- return node.children[0].content or ''
+ return node.children[0].content or ""
except:
- return node.content or ''
+ return node.content or ""
def html_escape(self, text):
- return text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
+ return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
def attr_escape(self, text):
- return self.html_escape(text).replace('"', '&quot')
+ return self.html_escape(text).replace('"', "&quot")
# *_emit methods for emitting nodes of the document
@@ -54,44 +55,44 @@ def text_emit(self, node):
return self.html_escape(node.content)
def separator_emit(self, node):
- return u'<hr>';
+ return u"<hr>"
def paragraph_emit(self, node):
- return u'<p>%s</p>\n' % self.emit_children(node)
+ return u"<p>%s</p>\n" % self.emit_children(node)
def bullet_list_emit(self, node):
- return u'<ul>\n%s</ul>\n' % self.emit_children(node)
+ return u"<ul>\n%s</ul>\n" % self.emit_children(node)
def number_list_emit(self, node):
- return u'<ol>\n%s</ol>\n' % self.emit_children(node)
+ return u"<ol>\n%s</ol>\n" % self.emit_children(node)
def list_item_emit(self, node):
- return u'<li>%s</li>\n' % self.emit_children(node)
+ return u"<li>%s</li>\n" % self.emit_children(node)
def table_emit(self, node):
- return u'<table>\n%s</table>\n' % self.emit_children(node)
+ return u"<table>\n%s</table>\n" % self.emit_children(node)
def table_row_emit(self, node):
- return u'<tr>%s</tr>\n' % self.emit_children(node)
+ return u"<tr>%s</tr>\n" % self.emit_children(node)
def table_cell_emit(self, node):
- return u'<td>%s</td>' % self.emit_children(node)
+ return u"<td>%s</td>" % self.emit_children(node)
def table_head_emit(self, node):
- return u'<th>%s</th>' % self.emit_children(node)
+ return u"<th>%s</th>" % self.emit_children(node)
def emphasis_emit(self, node):
- return u'<i>%s</i>' % self.emit_children(node)
+ return u"<i>%s</i>" % self.emit_children(node)
def strong_emit(self, node):
- return u'<b>%s</b>' % self.emit_children(node)
+ return u"<b>%s</b>" % self.emit_children(node)
def header_emit(self, node):
- return u'<h%d>%s</h%d>\n' % (
+ return u"<h%d>%s</h%d>\n" % (
node.level, self.html_escape(node.content), node.level)
def code_emit(self, node):
- return u'<tt>%s</tt>' % self.html_escape(node.content)
+ return u"<tt>%s</tt>" % self.html_escape(node.content)
def link_emit(self, node):
target = node.content
@@ -101,10 +102,10 @@ def link_emit(self, node):
inside = self.html_escape(target)
m = self.addr_re.match(target)
if m:
- if m.group('extern_addr'):
+ if m.group("extern_addr"):
return u'<a href="%s">%s</a>' % (
self.attr_escape(target), inside)
- elif m.group('inter_wiki'):
+ elif m.group("inter_wiki"):
raise NotImplementedError
return u'<a href="%s">%s</a>' % (
self.attr_escape(target), inside)
@@ -114,10 +115,10 @@ def image_emit(self, node):
text = self.get_text(node)
m = self.addr_re.match(target)
if m:
- if m.group('extern_addr'):
+ if m.group("extern_addr"):
return u'<img src="%s" alt="%s">' % (
self.attr_escape(target), self.attr_escape(text))
- elif m.group('inter_wiki'):
+ elif m.group("inter_wiki"):
raise NotImplementedError
return u'<img src="%s" alt="%s">' % (
self.attr_escape(target), self.attr_escape(text))
View
278 biblion/utils/mdx_codehilite.py
@@ -0,0 +1,278 @@
+#!/usr/bin/python
+import markdown
+
+"""
+File from https://code.achinghead.com/browser/py-markdown-ext/codehilite/trunk/mdx_codehilite.py?rev=22
+"""
+
+
+# --------------- CONSTANTS YOU MIGHT WANT TO MODIFY -----------------
+
+DEFAULT_HILITER = 'pygments' # one of 'enscript', 'dp', or 'pygments'
+try:
+ TAB_LENGTH = markdown.TAB_LENGTH
+except AttributeError:
+ TAB_LENGTH = 4
+
+# --------------- THE CODE -------------------------------------------
+# --------------- hiliter utility functions --------------------------
+def escape(txt) :
+ '''basic html escaping'''
+ txt = txt.replace('&', '&amp;')
+ txt = txt.replace('<', '&lt;')
+ txt = txt.replace('>', '&gt;')
+ txt = txt.replace('"', '&quot;')
+ return txt
+
+def number(txt):
+ '''use <ol> for line numbering'''
+ # Fix Whitespace
+ txt = txt.replace('\t', ' '*TAB_LENGTH)
+ txt = txt.replace(" "*4, "&nbsp; &nbsp; ")
+ txt = txt.replace(" "*3, "&nbsp; &nbsp;")
+ txt = txt.replace(" "*2, "&nbsp; ")
+
+ # Add line numbers
+ lines = txt.splitlines()
+ txt = '<div class="codehilite"<pre><ol>\n'
+ for line in lines:
+ txt += '\t<li>%s</li>\n'% line
+ txt += '</ol></pre></div>\n'
+ return txt
+
+# ---------------- The hiliters ---------------------------------------
+def enscript(src, lang=None, num=True):
+ '''
+Pass source code on to [enscript] (http://www.codento.com/people/mtr/genscript/)
+command line utility for hiliting.
+
+Usage:
+ >>> enscript(src [, lang [, num ]] )
+
+ @param src: Can be a string or any object with a .readline attribute.
+
+ @param lang: The language of code. Basic escaping only, if None.
+
+ @param num: (Boolen) Turns line numbering 'on' or 'off' (on by default).
+
+ @returns : A string of html.
+ '''
+ if lang:
+ cmd = 'enscript --highlight=%s --color --language=html --tabsize=%d --output=-'% (lang, TAB_LENGTH)
+ from os import popen3
+ (i, out, err) = popen3(cmd)
+ i.write(src)
+ i.close()
+ # check for errors
+ e = err.read()
+ if e != 'output left in -\n' :
+ # error - just escape
+ txt = escape(src)
+ else :
+ import re
+ pattern = re.compile(r'<PRE>(?P<code>.*?)</PRE>', re.DOTALL)
+ txt = pattern.search(out.read()).group('code')
+ # fix enscripts output
+ txt = txt.replace('\n</FONT></I>', '</FONT></I>\n').strip()
+ html_map = {'<I>' : '<em>',
+ '</I>' : '</em>',
+ '<B>' : '<strong>',
+ '</B>' : '</strong>',
+ '<FONT COLOR="#' : '<span style="color:#',
+ '</FONT>' : '</span>'
+ }
+ for k, v in html_map.items() :
+ txt = txt.replace(k, v)
+ else:
+ txt = escape(src)
+ if num :
+ txt = number(txt)
+ else :
+ txt = '<div class="codehilite"><pre>%s</pre></div>\n'% txt
+ return txt
+
+
+def dp(src, lang=None, num=True):
+ '''
+Pass source code to a textarea for the [dp.SyntaxHighlighter] (http://www.dreamprojections.com/syntaxhighlighter/Default.aspx)
+
+Usage:
+ >>> dp(src [, lang [, num ]] )
+
+ @param src: A string.
+
+ @param lang: The language of code. Undefined if None.
+
+ @param num: (Boolen) Turns line numbering 'on' or 'off' (on by default).
+
+ @returns : A string of html.
+ '''
+ gutter = ''
+ if not num:
+ gutter = ':nogutter'
+ if not lang:
+ lang = ''
+
+ return '<div class="codehilite"><textarea name="code" class="%s%s" cols="60" rows="10">\n%s\n</textarea></div>\n'% (lang, gutter, src)
+
+def pygment(src, lang = None, num = True):
+ '''
+Pass code to the [Pygments](http://pygments.pocoo.org/) highliter with
+optional line numbers. The output should then be styled with css to your liking.
+No styles are applied by default - only styling hooks (i.e.: <span class="k">).
+
+Usage:
+ >>> pygment(src [, lang [, num ]] )
+
+ @param src: Can be a string or any object with a .readline attribute.
+
+ @param lang: The language of code. Pygments will try to guess language if None.
+
+ @param num: (Boolen) Turns line numbering 'on' or 'off' (on by default).
+
+ @returns : A string of html.
+ '''
+ try:
+ from pygments import highlight
+ from pygments.lexers import get_lexer_by_name, guess_lexer, TextLexer
+ from pygments.formatters import HtmlFormatter
+ except ImportError:
+ # just escape and pass through
+ txt = escape(src)
+ if num:
+ txt = number(txt)
+ else :
+ txt = '<div class="codehilite"><pre>%s</pre></div>\n'% txt
+ return txt
+ else:
+ try:
+ lexer = get_lexer_by_name(lang)
+ except ValueError:
+ try:
+ lexer = guess_lexer(src)
+ except ValueError:
+ lexer = TextLexer()
+ formatter = HtmlFormatter(linenos=num, cssclass="codehilite")
+ return highlight(src, lexer, formatter)
+
+
+# ------------------ The Main CodeHilite Class ----------------------
+class CodeHilite:
+ '''
+A wrapper class providing a single API for various hilighting engines. Takes source code, determines which language it containes (if not provided), and passes it into the hiliter specified.
+
+Basic Usage:
+ >>> code = CodeHilite(src = text)
+ >>> html = code.hilite()
+
+ @param src: Can be a string or any object with a .readline attribute.
+
+ @param lang: A string. Accepted values determined by hiliter used. Overrides _getLang()
+
+ @param linenos: (Boolen) Turns line numbering 'on' or 'off' (off by default).
+
+ @param hiliter: A string. One of 'enscript', 'dp', or 'pygments'.
+
+Low Level Usage:
+ >>> code = CodeHilite()
+ >>> code.src = text # Can be a string or any object with a .readline attribute.
+ >>> code.lang = 'python' # Setting this will override _getLang()
+ >>> code.linenos = True # True or False; Turns line numbering on or off.
+ >>> code.hiliter = MyCustomHiliter # Where MyCustomHiliter is callable, takes three arguments (src, lang, linenos) and returns a string.
+ >>> html = code.hilite()
+ '''
+ def __init__(self, src=None, lang=None, linenos = False, hiliter=DEFAULT_HILITER):
+ self.src = src
+ self.lang = lang
+ self.linenos = linenos
+ # map of highlighters
+ hl_map = { 'enscript' : enscript, 'dp' : dp, 'pygments' : pygment }
+ try :
+ self.hiliter = hl_map[hiliter]
+ except KeyError:
+ raise "Please provide a valid hiliter as a string. One of 'enscript', 'dp', or 'pygments'"
+
+
+ def _getLang(self):
+ '''
+Determines language of a code block from shebang lines and whether said line should be removed or left in place. If the sheband line contains a path (even a single /) then it is assumed to be a real shebang lines and left alone. However, if no path is given (e.i.: #!python or :::python) then it is assumed to be a mock shebang for language identifitation of a code fragment and removed from the code block prior to processing for code highlighting. When a mock shebang (e.i: #!python) is found, line numbering is turned on. When colons are found in place of a shebang (e.i.: :::python), line numbering is left in the current state - off by default.
+ '''
+ import re
+
+ #split text into lines
+ lines = self.src.split("\n")
+ #pull first line to examine
+ fl = lines.pop(0)
+
+ c = re.compile(r'''
+ (?:(?:::+)|(?P<shebang>[#]!)) #shebang or 2 or more colons
+ (?P<path>(?:/\w+)*[/ ])? # zero or 1 path ending in either a / or a single space
+ (?P<lang>\w*) # the language (a single / or space before lang is a path)
+ ''', re.VERBOSE)
+ # search first line for shebang
+ m = c.search(fl)
+
+ if m:
+ # we have a match
+ try:
+ self.lang = m.group('lang').lower()
+ except IndexError:
+ self.lang = None
+ if m.group('path') and m.group('path').strip(): #We need to avoid matching empty paths in ":::\b*bash"
+ # path exists - restore first line
+ print m.group('path'), "path found for", m.group('lang')
+ lines.insert(0, fl)
+ if m.group('shebang'):
+ # shebang exists - use line numbers
+ self.linenos = True
+ else:
+ # No match
+ lines.insert(0, fl)
+
+ self.src = "\n".join(lines).rstrip("\n")
+
+ def hilite(self):
+ '''The wrapper function which brings it all togeather'''
+ self.src = self.src.strip('\n')
+
+ if not self.lang : self._getLang()
+
+ return self.hiliter(self.src, self.lang, self.linenos)
+
+
+# ------------------ The Markdown Extention -------------------------------
+class CodeHiliteExtention (markdown.Extension) :
+ def __init__(self, configs):
+ # define default configs
+ self.config = {'hiliter' : [DEFAULT_HILITER, "one of 'enscript', 'dp', or 'pygments'"],
+ 'force_linenos' : [False, "Force line numbers - Default: False"] }
+
+ # Override defaults with user settings
+ if configs:
+ for key, value in configs :
+ # self.config[key][0] = value
+ self.setConfig(key, value)
+
+ def extendMarkdown(self, md) :
+
+ def _hiliteCodeBlock(parent_elem, lines, inList):
+ """Overrides function of same name in standard Markdown class and
+ sends code blocks to a code highlighting proccessor. The result
+ is then stored in the HtmlStash, a placeholder is inserted into
+ the dom and the remainder of the text file is processed recursively.
+
+ @param parent_elem: DOM element to which the content will be added
+ @param lines: a list of lines
+ @param inList: a level
+ @returns: None"""
+ detabbed, theRest = md.blockGuru.detectTabbed(lines)
+ text = "\n".join(detabbed).rstrip()+"\n"
+ code = CodeHilite(text, hiliter=self.config['hiliter'][0], linenos=self.config['force_linenos'][0])
+ placeholder = md.htmlStash.store(code.hilite())
+ parent_elem.appendChild(md.doc.createTextNode(placeholder))
+ md._processSection(parent_elem, theRest, inList)
+
+ md._processCodeBlock = _hiliteCodeBlock
+
+def makeExtension(configs=None) :
+ return CodeHiliteExtention(configs=configs)
View
13 biblion/utils/slugify.py
@@ -0,0 +1,13 @@
+from django.template.defaultfilters import slugify
+
+
+def slugify_unique(value, model, slugfield="slug"):
+ suffix = 0
+ potential = base = slugify(value)
+ while True:
+ if suffix:
+ potential = "-".join([base, str(suffix)])
+ if not model.objects.filter(**{slugfield: potential}).count():
+ print model.objects.filter(**{slugfield: potential})
+ return potential
+ suffix += 1
View
13 biblion/utils/twitter.py
@@ -0,0 +1,13 @@
+from django.conf import settings
+
+try:
+ import twitter
+except ImportError:
+ twitter = None
+
+
+def can_tweet():
+ creds_available = (
+ hasattr(settings, "TWITTER_USERNAME") and hasattr(settings, "TWITTER_PASSWORD")
+ )
+ return twitter and creds_available
View
6 requirements.txt
@@ -1,6 +1,2 @@
-# required
-creole==1.2
-Pygments==1.2.2
-
# optional
-python-twitter==0.8.1
+python-twitter==0.8.1
View
25 setup.py
@@ -1,4 +1,4 @@
-from distutils.core import setup
+from setuptools import setup, find_packages
# see requirements.txt for dependencies
@@ -13,14 +13,14 @@
long_description = open("README.rst").read(),
license = "BSD",
url = "http://github.com/eldarion/biblion",
- packages = [
- "biblion",
+ packages=find_packages(),
+ install_requires=[
+ "creole==1.2",
+ "docutils==0.8.1",
+ "Markdown==2.0.3",
+ "Pygments==1.4",
+ "textile==2.1.5",
],
- package_data = {
- "biblion": [
- "templates/biblion/*.xml",
- ]
- },
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: Web Environment",
@@ -29,5 +29,12 @@
"Operating System :: OS Independent",
"Programming Language :: Python",
"Framework :: Django",
- ]
+ ],
+ # Make setuptools include all data files under version control,
+ # svn and CVS by default
+ include_package_data=True,
+ # Tells setuptools to download setuptools_git before running setup.py so
+ # it can find the data files under Git version control.
+ setup_requires=["setuptools_git"],
+ zip_safe=False,
)

0 comments on commit bc64605

Please sign in to comment.
Something went wrong with that request. Please try again.