commit 26333fe090cf4330f10fbf49e6a2de1d46f643b4 Author: Erik Stein Date: Mon Jan 8 13:51:34 2018 +0100 Initial import. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c158def --- /dev/null +++ b/.gitignore @@ -0,0 +1,62 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + + +./links/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b48430c --- /dev/null +++ b/LICENSE @@ -0,0 +1,8 @@ +MIT License +Copyright (c) 2008-2017 Erik Stein + +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/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..33a9ae1 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include AUTHORS +include LICENSE +include README.md +recursive-include shared/markup/templates * diff --git a/README.md b/README.md new file mode 100644 index 0000000..01ddb93 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# django-shared-markup + +Mix of Python and Django utility functions, classed etc. for handling of marked up text. \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c111c9e --- /dev/null +++ b/setup.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python + +from io import open +import os +from setuptools import setup, find_packages + + +def get_version(prefix): + import re + with open(os.path.join(prefix, '__init__.py')) as fd: + metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", fd.read())) + return metadata['version'] + + +def read(filename): + path = os.path.join(os.path.dirname(__file__), filename) + with open(path, encoding='utf-8') as handle: + return handle.read() + + +setup( + name='django-shared-markup', + version=get_version('shared/markup'), + description=' Mix of Python and Django utility functions, classed etc. for handling marked up text.', + long_description=read('README.md'), + author='Erik Stein', + author_email='erik@classlibrary.net', + url='https://projects.c--y.net/erik/django-shared-markup/', + license='MIT License', + platforms=['OS Independent'], + packages=find_packages( + exclude=['tests', 'testapp'], + ), + namespace_packages=['shared'], + include_package_data=True, + install_requires=[ + # 'django<2', commented out to make `pip install -U` easier + 'beautifulsoup4', + 'lxml', + ], + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3', + 'Topic :: Utilities', + ], + zip_safe=False, + tests_require=[ + 'Django', + # 'coverage', + ], + # test_suite='testapp.runtests.runtests', +) diff --git a/shared/__init__.py b/shared/__init__.py new file mode 100644 index 0000000..de40ea7 --- /dev/null +++ b/shared/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/shared/markup/__init__.py b/shared/markup/__init__.py new file mode 100644 index 0000000..5a6f84c --- /dev/null +++ b/shared/markup/__init__.py @@ -0,0 +1 @@ +__version__ = '0.5' diff --git a/shared/markup/content.py b/shared/markup/content.py new file mode 100644 index 0000000..9c4592a --- /dev/null +++ b/shared/markup/content.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.utils.safestring import mark_safe +from django.utils.html import conditional_escape, linebreaks + +from .utils import markdown_to_html + + +class MarkupContent(models.Model): + PLAIN_TEXT = 'text/plain' + MARKDOWN = 'text/x-markdown' + HTML = 'text/html' + MARKUP_FORMATS = ( + (MARKDOWN, _("Markdown")), + (PLAIN_TEXT, _("Reiner Text")), + (HTML, _("HTML")), + ) + markup_format = models.CharField(max_length=20, choices=MARKUP_FORMATS, default=MARKDOWN) + text = models.TextField() + + class Meta: + abstract = True + + def render(self, inline=False, **kwargs): + # TODO Use request? + + if self.markup_format == self.MARKDOWN: + # Marked safe by the markdown converter + return markdown_to_html(self.text, inline=inline) + + elif self.markup_format == self.HTML: + return mark_safe(self.text) + + else: + # TODO Use linebreaks filter + return linebreaks(conditional_escape(self.text)) diff --git a/shared/markup/markdown_utils.py b/shared/markup/markdown_utils.py new file mode 100644 index 0000000..e0a9156 --- /dev/null +++ b/shared/markup/markdown_utils.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +# Erik Stein , 2012-2016 + +import markdown as markdown_module +from django.utils.html import strip_tags +from django.utils.safestring import mark_safe +from shared.utils.text import html_entities_to_unicode + + +class PseudoParagraphProcessor(markdown_module.blockprocessors.ParagraphProcessor): + """ + Process paragraph blocks without producing HTML paragraphs. + """ + + def run(self, parent, blocks): + block = blocks.pop(0) + if block.strip(): + # Create element without enclosing tags + p = markdown_module.util.etree.SubElement(parent, None) + p.text = block.lstrip() + + +config = dict( + output_format='html5', + extensions=[ + 'markdown.extensions.extra', # Includes footnotes + 'markdown.extensions.nl2br', + 'markdown.extensions.sane_lists', + 'markdown.extensions.admonition', + 'markdown.extensions.smarty', + ] +) + +extensionConfigs = { + 'smarty': { + 'substitutions': { + 'left-single-quote': '‚', + 'right-single-quote': '‘', + 'left-double-quote': '„', + 'right-double-quote': '“' + } + } +} + + +markdown_processor = markdown_module.Markdown(**config) + +# Replace ParagraphProcessor +inline_markdown_processor = markdown_module.Markdown(**config) +inline_markdown_processor.parser.blockprocessors['paragraph'] = \ + PseudoParagraphProcessor(inline_markdown_processor.parser) + + +def markdown_to_inline_html(text, **kwargs): + kwargs['inline'] = True + return markdown_to_html(text, **kwargs) + +# TODO Decprecated API +inline_markdown = markdown_to_inline_html + + +def markdown_to_html(text, inline=False, **kwargs): + if inline: + processor = inline_markdown_processor + else: + processor = markdown_processor + processor.reset() + html = processor.convert(text, **kwargs) + return mark_safe(html) + +# TODO Decprecated API +markdown = markdown_to_html + + +def markdown_to_text(text, **kwargs): + """ + Converts a string from markdown to HTML, then removes all + HTML markup (tags and entities). + """ + html = markdown_to_html(text, **kwargs) + return strip_tags(html_entities_to_unicode(html)) diff --git a/shared/markup/markupfield/__init__.py b/shared/markup/markupfield/__init__.py new file mode 100644 index 0000000..a59c50c --- /dev/null +++ b/shared/markup/markupfield/__init__.py @@ -0,0 +1 @@ +__version__ = '1.0b2-erik' diff --git a/shared/markup/markupfield/fields.py b/shared/markup/markupfield/fields.py new file mode 100644 index 0000000..319b5a9 --- /dev/null +++ b/shared/markup/markupfield/fields.py @@ -0,0 +1,130 @@ +# -*- coding: UTF-8 -*- +# Erik Stein , 10/2010 + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from .markup import DEFAULT_MARKUP_TYPES, Markup +from .widgets import MarkupTextarea, AdminMarkupTextareaWidget + + +_get_markup_type_field_name = lambda name: '%s_markup_type' % name + +def _get_rendered_field_name(name): + field_name = '%s_rendered' % name + # Make the field internal + if not field_name[0] == '_': + field_name = '_%s' % field_name + return field_name + + +class MarkupDescriptor(object): + def __init__(self, field): + self.field = field + self.rendered_field_name = _get_rendered_field_name(self.field.name) + self.markup_type_field_name = _get_markup_type_field_name(self.field.name) + + def __get__(self, instance, owner): + if instance is None: + raise AttributeError("Can only be accessed via an instance.") + markup = instance.__dict__[self.field.name] + markup_type = instance.__dict__[self.markup_type_field_name] + if markup_type is None: + return None + if hasattr(self.field.markup_choices_dict[markup_type], 'render'): + markup_class = self.field.markup_choices_dict[markup_type] + else: + # Just a plain filter function, use default Markup class + if markup is None: + return None + markup_class = Markup + return markup_class(instance, self.field.name, self.rendered_field_name, + self.markup_type_field_name) + + def __set__(self, obj, value): + if isinstance(value, Markup): + obj.__dict__[self.field.name] = value.raw + setattr(obj, self.rendered_field_name, value.rendered) + setattr(obj, self.markup_type_field_name, value.markup_type) + else: + obj.__dict__[self.field.name] = value + + +class MarkupField(models.TextField): + def __init__(self, verbose_name=None, name=None, markup_type=None, + default_markup_type=None, markup_choices=DEFAULT_MARKUP_TYPES, + **kwargs): + if markup_type and default_markup_type: + raise ValueError("Cannot specify both markup_type and default_markup_type.") + # if markup_choices and not default_markup_type: + # raise ValueError('No default_markup_type specified.') + + self.default_markup_type = markup_type or default_markup_type + self.markup_type_editable = markup_type is None + + # pre 1.0 markup_choices might have been a dict + if isinstance(markup_choices, dict): + # raise DeprecationWarning('passing a dictionary as markup_choices is deprecated') + self.markup_choices_dict = markup_choices + self.markup_choices_list = markup_choices.keys() + else: + self.markup_choices_list = [mc[0] for mc in markup_choices] + self.markup_choices_dict = dict(markup_choices) + + if (self.default_markup_type and + self.default_markup_type not in self.markup_choices_list): + raise ValueError("Invalid default_markup_type '%s' for field '%s', allowed values: %s" % + (self.default_markup_type, name, ', '.join(self.markup_choices_list))) + super(MarkupField, self).__init__(verbose_name, name, **kwargs) + + def contribute_to_class(self, cls, name): + if not cls._meta.abstract: + column_name = self.db_column or name + choices = zip(self.markup_choices_list, self.markup_choices_list) + markup_type_field = models.CharField(max_length=30, + choices=choices, default=self.default_markup_type, + editable=self.markup_type_editable, blank=self.blank, + db_column=_get_markup_type_field_name(column_name)) + rendered_field = models.TextField(editable=False, + db_column=_get_rendered_field_name(column_name)) + markup_type_field.creation_counter = self.creation_counter+1 + rendered_field.creation_counter = self.creation_counter+2 + cls.add_to_class(_get_markup_type_field_name(name), markup_type_field) + cls.add_to_class(_get_rendered_field_name(name), rendered_field) + super(MarkupField, self).contribute_to_class(cls, name) + setattr(cls, self.name, MarkupDescriptor(self)) + + def pre_save(self, model_instance, add): + value = super(MarkupField, self).pre_save(model_instance, add) + if value.markup_type not in self.markup_choices_list: + raise ValueError('Invalid markup type (%s), allowed values: %s' % + (value.markup_type, + ', '.join(self.markup_choices_list))) + + if hasattr(self.markup_choices_dict[value.markup_type], 'render'): + rendered = value.render() + else: + rendered = self.markup_choices_dict[value.markup_type](value.raw) + setattr(model_instance, _get_rendered_field_name(self.attname), rendered) + return value.raw + + def get_db_prep_value(self, value): + # for Django 1.2+ rename this to get_prep_value + if isinstance(value, Markup): + return value.raw + else: + return value + + def value_to_string(self, obj): + value = self._get_val_from_obj(obj) + return value.raw + + def formfield(self, **kwargs): + defaults = {'widget': MarkupTextarea} + defaults.update(kwargs) + return super(MarkupField, self).formfield(**defaults) + + +# Register MarkupField to use the custom widget in the Admin +from django.contrib.admin.options import FORMFIELD_FOR_DBFIELD_DEFAULTS +FORMFIELD_FOR_DBFIELD_DEFAULTS[MarkupField] = {'widget': AdminMarkupTextareaWidget} diff --git a/shared/markup/markupfield/markup/__init__.py b/shared/markup/markupfield/markup/__init__.py new file mode 100644 index 0000000..65580f4 --- /dev/null +++ b/shared/markup/markupfield/markup/__init__.py @@ -0,0 +1,15 @@ +# -*- coding: UTF-8 -*- +# Erik Stein , 09/2010 + +from .base import Markup, PLAINTEXT_MARKUP_DESCRIPTION +from .html import HTMLMarkup, MARKUP_DESCRIPTION as HTML_MARKUP_DESCRIPTION +from .markdown import MarkdownMarkup, MARKUP_DESCRIPTION as MARKDOWN_MARKUP_DESCRIPTION +from .rst import RestructuredtextMarkup, MARKUP_DESCRIPTION as RST_MARKUP_DESCRIPTION + + +DEFAULT_MARKUP_TYPES = [ + PLAINTEXT_MARKUP_DESCRIPTION, + HTML_MARKUP_DESCRIPTION, + MARKDOWN_MARKUP_DESCRIPTION, + RST_MARKUP_DESCRIPTION, +] diff --git a/shared/markup/markupfield/markup/base b/shared/markup/markupfield/markup/base new file mode 100644 index 0000000..194d576 --- /dev/null +++ b/shared/markup/markupfield/markup/base @@ -0,0 +1,73 @@ +# -*- coding: UTF-8 -*- +# Erik Stein , 09/2010 + +from django.utils.translation import ugettext_lazy as _ +from django.utils.html import linebreaks, urlize + + +class Markup(object): + def __init__(self, instance, field_name, rendered_field_name, + markup_type_field_name): + # instead of storing actual values store a reference to the instance + # along with field names, this makes assignment possible + self.instance = instance + self.field_name = field_name + self.rendered_field_name = rendered_field_name + self.markup_type_field_name = markup_type_field_name + + def __unicode__(self): + # Allows display via templates to work without safe filter + return mark_safe(self.rendered) + + def __nonzero__(self): + """ + Returns truth depending on the 'raw' members truth. + """ + return bool(self.raw) + __bool__ = __nonzero__ # Python 3.x compatibility + + def __len__(self): + """ + Returns the length of the raw value. + """ + # TODO Decide if it's better to return the length of the rendered value. + return len(self.raw) + + def _get_raw(self): + return self.instance.__dict__[self.field_name] + + def _set_raw(self, value): + setattr(self.instance, self.field_name, value) + + raw = property(_get_raw, _set_raw) + + def _get_markup_type(self): + return self.instance.__dict__[self.markup_type_field_name] + + def _set_markup_type(self, value): + return setattr(self.instance, self.markup_type_field_name, value) + + markup_type = property(_get_markup_type, _set_markup_type) + + def _get_rendered(self): + # The rendered value is stored in a field of the model instance and is + # maintained by the MarkupField's pre_save method. + # The render() method must be called explicitely if the raw value + # is modified. + return getattr(self.instance, self.rendered_field_name) + + rendered = property(_get_rendered) + + def render(self, val): + # Must be implemented by subclasses and return the value for rendered self.raw + raise NotImplementedError + render.is_safe = True + + +class PlaintextMarkup(object): + def render(self, value): + return urlize(linebreaks(markup)) + render.is_safe = True + + +PLAINTEXT_MARKUP_DESCRIPTION = ('text', PlaintextMarkup) diff --git a/shared/markup/markupfield/markup/base.py b/shared/markup/markupfield/markup/base.py new file mode 100644 index 0000000..006e185 --- /dev/null +++ b/shared/markup/markupfield/markup/base.py @@ -0,0 +1,56 @@ +# -*- coding: UTF-8 -*- +# Erik Stein , 09/2010 + +from django.utils.translation import ugettext_lazy as _ +from django.utils.html import linebreaks, urlize + + +class Markup(object): + def __init__(self, instance, field_name, rendered_field_name, + markup_type_field_name): + # instead of storing actual values store a reference to the instance + # along with field names, this makes assignment possible + self.instance = instance + self.field_name = field_name + self.rendered_field_name = rendered_field_name + self.markup_type_field_name = markup_type_field_name + + # raw is read/write + def _get_raw(self): + return self.instance.__dict__[self.field_name] + def _set_raw(self, value): + setattr(self.instance, self.field_name, value) + raw = property(_get_raw, _set_raw) + + # markup_type is read/write + def _get_markup_type(self): + return self.instance.__dict__[self.markup_type_field_name] + def _set_markup_type(self, value): + return setattr(self.instance, self.markup_type_field_name, value) + markup_type = property(_get_markup_type, _set_markup_type) + + # rendered is a read only property + def _get_rendered(self): + return getattr(self.instance, self.rendered_field_name) + rendered = property(_get_rendered) + + # allows display via templates to work without safe filter + def __unicode__(self): + return self.rendered + __unicode__.is_safe = True + + def render(self, value): + """ + Must be implemented by subclasses and return the rendered self.raw + """ + raise NotImplementedError + render.is_safe = True + + +class PlaintextMarkup(object): + def render(self, value): + return urlize(linebreaks(markup)) + render.is_safe = True + + +PLAINTEXT_MARKUP_DESCRIPTION = ('text', PlaintextMarkup) diff --git a/shared/markup/markupfield/markup/html.py b/shared/markup/markupfield/markup/html.py new file mode 100644 index 0000000..b231b64 --- /dev/null +++ b/shared/markup/markupfield/markup/html.py @@ -0,0 +1,16 @@ +# -*- coding: UTF-8 -*- +# Erik Stein , 06/2010 + +from django.utils.translation import ugettext_lazy as _ + +from .base import Markup + + +class HTMLMarkup(Markup): + def render(self): + # HTML of course doesn't need to be converted to HTML + return self.raw or u"" # Make sure that the return value is text + render.is_safe = True + + +MARKUP_DESCRIPTION = ('text/html', HTMLMarkup) diff --git a/shared/markup/markupfield/markup/markdown.py b/shared/markup/markupfield/markup/markdown.py new file mode 100644 index 0000000..223c567 --- /dev/null +++ b/shared/markup/markupfield/markup/markdown.py @@ -0,0 +1,29 @@ +# -*- coding: UTF-8 -*- +# Erik Stein , 09/2010 + +import markdown +from django.utils.functional import curry +from django.utils.translation import ugettext_lazy as _ + +from .base import Markup +from .pygments import PYGMENTS_INSTALLED + + +md_filter = markdown.markdown + +# try and replace if pygments & codehilite are available +if PYGMENTS_INSTALLED: + try: + from markdown.extensions.codehilite import makeExtension + md_filter = curry(markdown.markdown, extensions=['codehilite(css_class=highlight)']) + except ImportError: + pass + + +class MarkdownMarkup(Markup): + def render(self): + return md_filter(self.raw) + render.is_safe = True + + +MARKUP_DESCRIPTION = ('text/x-markdown', MarkdownMarkup) diff --git a/shared/markup/markupfield/markup/pygments.py b/shared/markup/markupfield/markup/pygments.py new file mode 100644 index 0000000..0f90018 --- /dev/null +++ b/shared/markup/markupfield/markup/pygments.py @@ -0,0 +1,9 @@ +# -*- coding: UTF-8 -*- +# Erik Stein , 09/2010 + +try: + import pygments + PYGMENTS_INSTALLED = True + +except ImportError: + PYGMENTS_INSTALLED = False diff --git a/shared/markup/markupfield/markup/rst/__init__.py b/shared/markup/markupfield/markup/rst/__init__.py new file mode 100644 index 0000000..763e756 --- /dev/null +++ b/shared/markup/markupfield/markup/rst/__init__.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# Erik Stein , 09/2010 + +import docutils +import docutils.parsers.rst +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + +from ..base import Markup +from ...settings import (RST_INITIAL_HEADER_LEVEL, RST_WRITER_NAME, + RST_DEFAULT_LANGUAGE_CODE, RST_DOCTITLE_XFORM, + RST_INPUT_ENCODING, RST_DEBUG_LEVEL, RST_FILTER_SETTINGS) +from ..pygments import PYGMENTS_INSTALLED + + +# Let's you conveniently import the parser +parser = docutils.parsers.rst + +try: + if PYGMENTS_INSTALLED: + # Register "code" directive for pygments formatting + from pygments import highlight + from pygments.lexers import get_lexer_by_name, TextLexer + from pygments.formatters import HtmlFormatter + + DEFAULT = HtmlFormatter() + VARIANTS = { + 'linenos': HtmlFormatter(linenos=True), + } + + 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: + # no lexer found - use the text one instead of an exception + lexer = TextLexer() + formatter = options and VARIANTS[options.keys()[0]] or DEFAULT + parsed = highlight(u'\n'.join(content), lexer, formatter) + return [docutils.nodes.raw('', parsed, format='html')] + pygments_directive.arguments = (1, 0, 1) + pygments_directive.content = 1 + parser.directives.register_directive('code', pygments_directive) +except ImportError: + pass + + +class RestructuredtextMarkup(Markup): + docutils_settings = { + 'language_code': RST_DEFAULT_LANGUAGE_CODE, + 'doctitle_xform': RST_DOCTITLE_XFORM, + 'input_encoding': RST_INPUT_ENCODING, + 'initial_header_level': RST_INITIAL_HEADER_LEVEL, + 'report_level': RST_DEBUG_LEVEL, + } + docutils_settings.update(RST_FILTER_SETTINGS) + + def render(self, initial_header_level=RST_INITIAL_HEADER_LEVEL, **kwargs): + """ + Returns the rendered html fragment, i.e. without any html header part. + """ + settings = self.docutils_settings.copy() + settings['initial_header_level'] = initial_header_level + parts = docutils.core.publish_parts( + source=self.raw, + writer_name=WRITER_NAME, + settings_overrides=settings + ) + return parts['fragment'] + render.is_safe = True + + def doctree(self, **kwargs): + """ + Returns the docutils doctree. + """ + return docutils.core.publish_doctree(self.raw, settings_overrides=self.docutils_settings) + + def title(self, **kwargs): + """ + Returns the first found title node of a docutils doctree. + """ + # TODO Why don't we use the 'title' part? + document = self.doctree() + matches = document.traverse(condition=lambda node: isinstance(node, docutils.nodes.title)) + if len(matches): + return matches[0].astext() + else: + return None + + def plaintext(self, **kwargs): + return self.doctree().astext() + + +# Convenience function +def restructuredtext(text, **kwargs): + rst = RestructuredtextMarkup() + rst.raw = text + return rst.render(**kwargs) + + +MARKUP_DESCRIPTION = ('text/x-rst', RestructuredtextMarkup) diff --git a/shared/markup/markupfield/markup/rst/helpers.py b/shared/markup/markupfield/markup/rst/helpers.py new file mode 100644 index 0000000..df72cb2 --- /dev/null +++ b/shared/markup/markupfield/markup/rst/helpers.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# 09/2009 Erik Stein + + +def image_references(image_qs): + """ + Returns a ReStructured source fragment containing image references + for the given Image queryset. + """ + def make_image_reference(image_obj): + markup = u".. |%s| image:: %s" % (image_obj.slug, image_obj.imagefile.url) + cl = image_obj.caption_line() + if cl: + markup += u"\n :alt: %s" + return markup + return u"\n".join([make_image_reference(img) for img in image_qs]) + diff --git a/shared/markup/markupfield/settings.py b/shared/markup/markupfield/settings.py new file mode 100644 index 0000000..eddc010 --- /dev/null +++ b/shared/markup/markupfield/settings.py @@ -0,0 +1,37 @@ +# -*- coding: UTF-8 -*- +# Erik Stein , 09/2010 +""" +Use PREFIX + variable name in your settings file. + +Example:: + + MARKUP_MARKUP_TYPES + +""" + +import sys +from django.utils.translation import ugettext as _ +from django.conf import settings as project_settings + + +_PREFIX = 'MARKUP_' + +defaults = { + 'RST_DEFAULT_LANGUAGE_CODE': getattr(project_settings, 'LANGUAGE_CODE', 'en').split('-')[0], + 'RST_WRITER_NAME': 'html', # 'html4css1' + 'RST_INITIAL_HEADER_LEVEL': 3, + 'RST_DOCTITLE_XFORM': False, # Don't use first section title as document title + 'RST_INPUT_ENCODING': 'utf-8', + 'RST_DEBUG_LEVEL': getattr(project_settings, 'RST_DEBUG_LEVEL', project_settings.DEBUG and 1 or 5), + 'RST_FILTER_SETTINGS': {}, +} + +__all__ = [defaults] + + +# Setting up module constants + +module = sys.modules[__name__] +for setting_name, default_value in defaults.iteritems(): + setattr(module, setting_name, getattr(project_settings, _PREFIX + setting_name, default_value)) + __all__.append(getattr(module, setting_name)) diff --git a/shared/markup/markupfield/tests/__init__.py b/shared/markup/markupfield/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shared/markup/markupfield/tests/models.py b/shared/markup/markupfield/tests/models.py new file mode 100644 index 0000000..73873bc --- /dev/null +++ b/shared/markup/markupfield/tests/models.py @@ -0,0 +1,41 @@ +from django.db import models + +from markupfield.fields import MarkupField +from markupfield.markup import markdown, rst + + +CUSTOM_MARKUP_TYPES = ( + ('markdown', markdown.MarkdownMarkup), + rst.MARKUP_DESCRIPTION, +) + + +class Post(models.Model): + title = models.CharField(max_length=50) + body = MarkupField('body of post') + + def __unicode__(self): + return self.title + + +class Article(models.Model): + normal_field = MarkupField() + markup_choices_field = MarkupField(markup_choices=(('pandamarkup', lambda x: 'panda'), + ('nomarkup', lambda x: x))) + default_field = MarkupField(default_markup_type='text/x-markdown') + markdown_field = MarkupField(markup_type='text/x-markdown') + + +class Abstract(models.Model): + content = MarkupField() + + class Meta: + abstract = True + + +class Concrete(Abstract): + pass + + +class CustomArticle(models.Model): + text = MarkupField(markup_choices=CUSTOM_MARKUP_TYPES, default_markup_type='text/x-rst') diff --git a/shared/markup/markupfield/tests/run_tests.sh b/shared/markup/markupfield/tests/run_tests.sh new file mode 100755 index 0000000..79f3368 --- /dev/null +++ b/shared/markup/markupfield/tests/run_tests.sh @@ -0,0 +1 @@ +django-admin.py test --settings=markupfield.tests.settings --pythonpath=../.. diff --git a/shared/markup/markupfield/tests/settings.py b/shared/markup/markupfield/tests/settings.py new file mode 100644 index 0000000..396f15b --- /dev/null +++ b/shared/markup/markupfield/tests/settings.py @@ -0,0 +1,24 @@ + +DATABASE_ENGINE = 'sqlite3' +DATABASE_NAME = 'markuptest.db' + +try: + import markdown +except ImportError: + class markdown(object): + def markdown(self): + return '' +from docutils.core import publish_parts + +def render_rest(markup): + parts = publish_parts(source=markup, writer_name="html4css1") + return parts["fragment"] + +MARKUP_FIELD_TYPES = [ + ('markdown', markdown.markdown), + ('ReST', render_rest), +] + +INSTALLED_APPS = ( + 'markupfield.tests', +) diff --git a/shared/markup/markupfield/tests/tests.py b/shared/markup/markupfield/tests/tests.py new file mode 100644 index 0000000..da8b6c9 --- /dev/null +++ b/shared/markup/markupfield/tests/tests.py @@ -0,0 +1,129 @@ +from django.test import TestCase +from django.core import serializers +from markupfield.fields import MarkupField +from markupfield.markup import Markup, RestructuredtextMarkup +from markupfield.widgets import MarkupTextarea, AdminMarkupTextareaWidget +from markupfield.tests.models import Post, Article, Concrete, CustomArticle + +from django.forms.models import modelform_factory + + +ArticleForm = modelform_factory(Article) + + +class MarkupFieldTestCase(TestCase): + def setUp(self): + self.mp = Post(title='example markdown post', body='**markdown**', + body_markup_type='text/x-markdown') + self.mp.save() + self.rp = Post(title='example restructuredtext post', body='*ReST*', body_markup_type='ReST') + self.rp.save() + + def test_verbose_name(self): + self.assertEquals(self.mp._meta.get_field('body').verbose_name, 'body of post') + + def test_markup_body(self): + self.assertEquals(self.mp.body.raw, '**markdown**') + self.assertEquals(self.mp.body.rendered, '

markdown

') + self.assertEquals(self.mp.body.markup_type, 'text/x-markdown') + + def test_markup_unicode(self): + u = unicode(self.rp.body.rendered) + self.assertEquals(u, u'

ReST

\n') + + def test_from_database(self): + " Test that data loads back from the database correctly and 'post' has the right type." + p1 = Post.objects.get(pk=self.mp.pk) + self.assert_(isinstance(p1.body, Markup)) + self.assertEquals(unicode(p1.body), u'

markdown

') + + ## Assignment ## + def test_body_assignment(self): + self.rp.body = '**ReST**' + self.rp.save() + self.assertEquals(unicode(self.rp.body), u'

ReST

\n') + + def test_raw_assignment(self): + self.rp.body.raw = '*ReST*' + self.rp.save() + self.assertEquals(unicode(self.rp.body), u'

ReST

\n') + + def test_rendered_assignment(self): + def f(): + self.rp.body.rendered = 'this should fail' + self.assertRaises(AttributeError, f) + + def test_body_type_assignment(self): + self.rp.body.markup_type = 'text/x-markdown' + self.rp.save() + self.assertEquals(self.rp.body.markup_type, 'text/x-markdown') + self.assertEquals(unicode(self.rp.body), u'

ReST

') + + ## Serialization ## + + def test_serialize_to_json(self): + stream = serializers.serialize('json', Post.objects.all()) + self.assertEquals(stream, '[{"pk": 1, "model": "tests.post", "fields": {"body": "**markdown**", "_body_rendered": "

markdown

", "body_markup_type": "text/x-markdown", "title": "example markdown post"}}, {"pk": 2, "model": "tests.post", "fields": {"body": "*ReST*", "_body_rendered": "

ReST

\\n", "body_markup_type": "ReST", "title": "example restructuredtext post"}}]') + + def test_deserialize_json(self): + stream = serializers.serialize('json', Post.objects.all()) + obj = list(serializers.deserialize('json', stream))[0] + self.assertEquals(obj.object, self.mp) + + ## Other ## + + def test_inheritance(self): + # test that concrete correctly got the added fields + concrete_fields = [f.name for f in Concrete._meta.fields] + self.assertEquals(concrete_fields, ['id', 'content', 'content_markup_type', '_content_rendered']) + + def test_markup_type_validation(self): + self.assertRaises(ValueError, MarkupField, 'verbose name', 'markup_field', 'bad_markup_type') + + def test_custom_markup_class(self): + complex_rest = "Title of the article\n====================\n\nA paragraph with an *emphasized text*.\n\n" + a = CustomArticle(text=complex_rest) + a.save() + self.assertEquals(type(a.text), RestructuredtextMarkup) + self.assertEquals(a.text.rendered, + u'
\n

Title of the article

\n

A paragraph with an emphasized text.

\n
\n') + self.assertEquals(a.text.plaintext(), + u'Title of the article\n\nA paragraph with an emphasized text.') + self.assertEquals(a.text.title(), + u'Title of the article') + + +class MarkupWidgetTests(TestCase): + def test_markuptextarea_used(self): + self.assert_(isinstance(MarkupField().formfield().widget, MarkupTextarea)) + self.assert_(isinstance(ArticleForm()['normal_field'].field.widget, MarkupTextarea)) + + def test_markuptextarea_render(self): + a = Article(normal_field='**normal**', normal_field_markup_type='text/x-markdown', + default_field='**default**', markdown_field='**markdown**', + markup_choices_field_markup_type='nomarkup') + a.save() + af = ArticleForm(instance=a) + self.assertEquals(unicode(af['normal_field']), u'') + + def test_no_markup_type_field_if_set(self): + 'ensure that a field with non-editable markup_type set does not have a _markup_type field' + self.assert_('markdown_field_markup_type' not in ArticleForm().fields.keys()) + + def test_markup_type_choices(self): + self.assertEquals(ArticleForm().fields['normal_field_markup_type'].choices, + [('text/x-markdown', 'text/x-markdown'), ('text/x-rst', 'text/x-rst')]) + self.assertEquals(ArticleForm().fields['markup_choices_field_markup_type'].choices, + [('pandamarkup', 'pandamarkup'), ('nomarkup', 'nomarkup')]) + + def test_default_markup_type(self): + self.assert_(ArticleForm().fields['normal_field_markup_type'].initial is None) + self.assertEqual(ArticleForm().fields['default_field_markup_type'].initial, 'text/x-markdown') + + def test_model_admin_field(self): + # borrows from regressiontests/admin_widgets/tests.py + from django.contrib import admin + ma = admin.ModelAdmin(Post, admin.site) + self.assert_(isinstance(ma.formfield_for_dbfield(Post._meta.get_field('body')).widget, + AdminMarkupTextareaWidget)) + diff --git a/shared/markup/markupfield/widgets.py b/shared/markup/markupfield/widgets.py new file mode 100644 index 0000000..340bbc1 --- /dev/null +++ b/shared/markup/markupfield/widgets.py @@ -0,0 +1,16 @@ +# -*- coding: UTF-8 -*- +# Erik Stein , 10/2010 + +from django import forms +from django.contrib.admin.widgets import AdminTextareaWidget + + +class MarkupTextarea(forms.widgets.Textarea): + def render(self, name, value, attrs=None): + if value is not None and not isinstance(value, unicode): + value = value.raw + return super(MarkupTextarea, self).render(name, value, attrs) + + +class AdminMarkupTextareaWidget(MarkupTextarea, AdminTextareaWidget): + pass diff --git a/shared/markup/templatetags/__init__.py b/shared/markup/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shared/markup/templatetags/markup_tags.py b/shared/markup/templatetags/markup_tags.py new file mode 100644 index 0000000..71bcf2a --- /dev/null +++ b/shared/markup/templatetags/markup_tags.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +# Erik Stein , 2015 + +import re +from django import template +from django.template.defaultfilters import stringfilter +from django.utils.html import conditional_escape +from .. import markdown_utils + + +register = template.Library() + + +@register.filter(needs_autoescape=False) +@stringfilter +def inline_markdown(text, autoescape=None): + """ Doesn't wrap the markup in a HTML paragraph. """ + if autoescape: + esc = conditional_escape + else: + esc = lambda x: x + return markdown_utils.markdown_to_inline_html(esc(text)) + + +@register.filter(needs_autoescape=False) +@stringfilter +def markdown(text, autoescape=None): + if autoescape: + esc = conditional_escape + else: + esc = lambda x: x + return markdown_utils.markdown_to_html(esc(text)) + + +@register.filter(needs_autoescape=True) +@stringfilter +def markdown_to_text(text, autoescape=None): + """ + Converts a string from markdown to HTML, then removes all + HTML markup (tags and entities). + """ + if autoescape: + esc = conditional_escape + else: + esc = lambda x: x + return markdown_utils.markdown_to_text(esc(text)) + + +urlfinder = re.compile('^(http:\/\/\S+)') +urlfinder2 = re.compile('\s(http:\/\/\S+)') + + +@register.filter('urlify_markdown') +def urlify_markdown(value): + value = urlfinder.sub(r'<\1>', value) + return urlfinder2.sub(r' <\1>', value)