├── .git-blame-ignore-revs
├── .github
└── workflows
│ ├── linting.yml
│ └── push.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── django_markup
├── __init__.py
├── defaults.py
├── fields.py
├── filter
│ ├── __init__.py
│ ├── creole_filter.py
│ ├── linebreaks_filter.py
│ ├── markdown_filter.py
│ ├── none_filter.py
│ ├── rst_filter.py
│ ├── smartypants_filter.py
│ ├── textile_filter.py
│ └── widont_filter.py
├── markup.py
├── templatetags
│ ├── __init__.py
│ └── markup_tags.py
└── tests
│ ├── __init__.py
│ ├── files
│ ├── rst_header.txt
│ ├── rst_header_expected.txt
│ ├── rst_raw.txt
│ ├── rst_with_pygments.txt
│ └── rst_with_pygments_expected.txt
│ ├── markup_strings.py
│ ├── settings.py
│ ├── templates
│ ├── test_templatetag.html
│ └── test_templatetag_filterwrapper.html
│ ├── test_custom_filter.py
│ ├── test_filter.py
│ └── test_templatetag.py
├── docs
├── _static
│ ├── basic.css
│ ├── default.css
│ └── pygments.css
├── bundled_filter
│ ├── creole.rst
│ ├── linebreaks.rst
│ ├── markdown.rst
│ ├── none.rst
│ ├── rst.rst
│ ├── smartypants.rst
│ ├── textile.rst
│ └── widont.rst
├── conf.py
├── configuration.rst
├── filter.rst
├── filter_settings.rst
├── formatter.rst
├── index.rst
├── installation.rst
├── usage_models.rst
├── usage_python.rst
└── usage_templates.rst
├── pyproject.toml
└── tox.ini
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # Black the entire project
2 | af5c4b8d50ca9aa512e1cf6af5bab4cbaaa2898a
3 |
--------------------------------------------------------------------------------
/.github/workflows/linting.yml:
--------------------------------------------------------------------------------
1 | name: Code Quality Checks
2 |
3 | on:
4 | push:
5 |
6 | jobs:
7 | linting:
8 | name: Code Quality Checks
9 | runs-on: ubuntu-22.04
10 | steps:
11 | - name: Checkout Code
12 | uses: actions/checkout@v4
13 |
14 | - name: Install Dependencies
15 | run: pip install "ruff" "mypy==1.5.1"
16 |
17 | - name: Code Linting
18 | run: ruff check django_markup
19 |
20 | - name: Code Formatting
21 | run: ruff format --check django_markup
22 |
23 | - name: Type Check
24 | run: mypy --install-types --non-interactive django_markup
25 |
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | name: Testsuite Run
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | max-parallel: 4
10 | matrix:
11 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Set up Python ${{ matrix.python-version }}
17 | uses: actions/setup-python@v5
18 | with:
19 | python-version: ${{ matrix.python-version }}
20 |
21 | - name: Install dependencies
22 | run: |
23 | python -m pip install --upgrade pip
24 | pip install tox tox-gh-actions
25 | - name: Test with tox
26 | run: tox
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | *.pyc
3 | .coverage
4 | .eggs
5 | build/
6 | dist/
7 | docs/_build
8 | poetry.lock
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v1.9.1 (2024-11-19):
4 |
5 | - Drop support for Python <= 3.8.
6 | - Drop support for Django <= 4.1.
7 | - Added support for Python 3.13.
8 |
9 | ## v1.9 (2024-08-11):
10 |
11 | - Minor Type Annotation fixes.
12 | - Switch from pipenv to Poetry.
13 | - Added support for Django 5.1.
14 |
15 | ## v1.8.1 (2023-10-07):
16 |
17 | - Remove all annotations for "Self". It would require an additional dependency for
18 | installations on Python <3.11 and that's not worth it.
19 |
20 | ## v1.8 (2023-10-07):
21 |
22 | - Drop support for Python <3.8.
23 | - Added support for Python 3.12 and Django 5.0.
24 | - Type Annotations.
25 |
26 | ## v1.7.2 (2023-05-01):
27 |
28 | - Fixed a setup.cfg bug that defined the minimal Django version to be v3.7 which does
29 | not exist. The correct version is 3.2.
30 |
31 | ## v1.7.1 (2023-04-25):
32 |
33 | - Fixed Python classifiers in setup.cfg.
34 |
35 | ## v1.7 (2023-04-25):
36 |
37 | - Django 4.2 compatibility and tests.
38 | - Python 3.11 compatibility and tests.
39 |
40 | ## v1.6 (2022-08-14):
41 |
42 | - Dropped support for Django <3.2 and Python <3.7.
43 | - Django 3.2 (LTS) compatibility and tests.
44 | - Django 4.0 compatibility and tests.
45 | - Django 4.1 compatibility and tests.
46 | - Python 3.9 compatibility and tests.
47 | - Python 3.10 compatibility and tests.
48 |
49 |
50 | ## v1.5 (2020-06-12):
51 |
52 | - Dropped support for Django <=1.11 and Python <=3.5.
53 | - Python 3.8 compatibility and tests.
54 | - Django 3.0 compatibility and tests.
55 | - bleach-whitelist dependency is no longer necessary as tags are now shipped
56 | with the built-in markdown filter.
57 | - Uses pytest for testing.
58 |
59 | ## v1.4 (2019-03-15):
60 |
61 | - Markdown's safe_mode was deprecated and no longer functional, it's behavior
62 | was replaced with [bleach].
63 | - Pipfile support for local development and general code cleanup.
64 |
65 | [bleach]: https://github.com/mozilla/bleach
66 |
67 | ## v1.3 (2018-09-07):
68 |
69 | - Python 3.6 and 3.7 compatibility and tests.
70 | - Django 2.0 and 2.1 compatibility and tests.
71 | - The package setup script now provides the ability to install all filter
72 | dependencies automatically. See the installation Readme for details.
73 |
74 | ## v1.2 (2017-03-18):
75 |
76 | - Django 1.10 compatibility and tests.
77 | - Updated all filter dependencies. most notably SmartyPants to v2.0
78 | which changed it's API, so your project dependencies need to update it
79 | as well.
80 |
81 | ## v1.1 (2016-05-02):
82 |
83 | - The Markdown filter has the ``safe_mode`` option enabled by default.
84 | - The RestructuredText filter has the file and raw content inclusion
85 | disabled by default.
86 |
87 | ## v1.0 (2016-01-02):
88 |
89 | - Removed some 5 year old dust
90 | - Django 1.8+ compatible
91 | - Tests
92 |
93 | Backwards incompatible changes:
94 |
95 | - Removed Pygments highlighting in the Markdown and RestructuredText filter.
96 | - Removed CreoleParser library in favor of a pypi package.
97 | - Removed Lightbox filter.
98 | - The RestructuredText filter now renders level 1 and 2 headers.
99 | See Github [Issue 14] for details and a backwards compatible workaround.
100 |
101 | ## v0.4 (2011-06-01):
102 |
103 | - Added a widont filter
104 | - MarkupField is South compatible.
105 | - Tested with Django 1.3
106 |
107 | ## v0.3 (2009-07-29):
108 |
109 | django-markup now ships with a builtin creole parser. Advantage is, that
110 | the recently used Creoleparser library needs the Genshi lib, which needs
111 | a c-compiler and so on. The builtin creole parser is a pure python library
112 | without any dependencies and follows the wikicreole.org specifications.
113 | django-markup uses the [WikiCreole library].
114 |
115 | [WikiCreole library]: http://devel.sheep.art.pl/creole/
116 | [Issue 14]: https://github.com/bartTC/django-markup/issues/14
117 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009, Martin Mahner
2 |
3 | 📖 Full documentation on https://django-markup.readthedocs.io/en/latest/
4 |
5 | # django-markup
6 |
7 | This app is a generic way to provide filters that convert text into html.
8 |
9 | ## Compatibility Matrix:
10 |
11 | | Py/Dj | 3.9 | 3.10 | 3.11 | 3.12 | 3.13 |
12 | |-----------|-----|------|------|------|------|
13 | | 4.2 (LTS) | ✓ | ✓ | ✓ | ✓ | ✓ |
14 | | 5.0 | — | ✓ | ✓ | ✓ | ✓ |
15 | | 5.1 | — | ✓ | ✓ | ✓ | ✓ |
16 |
17 | ## Quickstart
18 |
19 | Download and install the package from the python package index (pypi):
20 |
21 | Note that `django-markup` ships with some filters ready to use, but the more
22 | complex packages such as Markdown or ReStructuredText are not part of the code.
23 | Please refer the docs which packages are used for the built-in filter.
24 |
25 | An alternative is to install django-markup with all filter dependencies
26 | right away. Do so with:
27 |
28 | $ pip install django-markup[all_filter_dependencies]
29 |
30 | Then add it to the ``INSTALLED_APPS`` list:
31 |
32 | INSTALLED_APPS = (
33 | ...
34 | 'django_markup',
35 | )
36 |
37 | Use it in the template:
38 |
39 | {% load markup_tags %}
40 | {{ the_text|apply_markup:"markdown" }}
41 |
42 | Or in Python code:
43 |
44 | from django_markup.markup import formatter
45 | formatter('Some *Markdown* text.', filter_name='markdown')
46 |
47 | # Testsuite
48 |
49 | To run the testsuite install the project with pipenv and run it:
50 |
51 | % pipenv install --dev
52 | $ pipenv run test
53 |
54 | You can also test against a variation of Django and Python versions
55 | using tox:
56 |
57 | $ tox
58 |
59 |
--------------------------------------------------------------------------------
/django_markup/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartTC/django-markup/7c94ca809fcfd16c532c76b8089140100c8a5cfd/django_markup/__init__.py
--------------------------------------------------------------------------------
/django_markup/defaults.py:
--------------------------------------------------------------------------------
1 | # The list of automatically loaded MarkupFilters
2 | from django_markup.filter.creole_filter import CreoleMarkupFilter
3 | from django_markup.filter.linebreaks_filter import LinebreaksMarkupFilter
4 | from django_markup.filter.markdown_filter import MarkdownMarkupFilter
5 | from django_markup.filter.none_filter import NoneMarkupFilter
6 | from django_markup.filter.rst_filter import RstMarkupFilter
7 | from django_markup.filter.smartypants_filter import SmartyPantsMarkupFilter
8 | from django_markup.filter.textile_filter import TextileMarkupFilter
9 | from django_markup.filter.widont_filter import WidontMarkupFilter
10 |
11 | # MarkupFilter that get's loaded automatically
12 | # You can override this list within your settings: MARKUP_FILTER
13 |
14 | DEFAULT_MARKUP_FILTER = {
15 | "creole": CreoleMarkupFilter,
16 | "linebreaks": LinebreaksMarkupFilter,
17 | "markdown": MarkdownMarkupFilter,
18 | "none": NoneMarkupFilter,
19 | "restructuredtext": RstMarkupFilter,
20 | "smartypants": SmartyPantsMarkupFilter,
21 | "textile": TextileMarkupFilter,
22 | "widont": WidontMarkupFilter,
23 | }
24 |
25 | # MarkupFilter that are the default value for choices, used in the MarkupField
26 | # You can override this list within your settings: MARKUP_CHOICES
27 |
28 | DEFAULT_MARKUP_CHOICES = ("none", "linebreaks", "markdown", "restructuredtext")
29 |
--------------------------------------------------------------------------------
/django_markup/fields.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Any
4 |
5 | from django.db.models.fields import CharField
6 | from django.utils.translation import gettext_lazy
7 |
8 | from django_markup.markup import MarkupFormatter, UnregisteredFilterError, formatter
9 |
10 |
11 | class MarkupField(CharField):
12 | """
13 | A CharField that holds the markup name for the row. In the admin it's
14 | displayed as a ChoiceField.
15 | """
16 |
17 | def __init__(
18 | self,
19 | default: str | None = None,
20 | formatter: MarkupFormatter = formatter,
21 | *args: Any,
22 | **kwargs: Any,
23 | ) -> None:
24 | # Check that the default value is a valid filter
25 | if default:
26 | if default not in formatter.filter_list:
27 | msg = (
28 | f"'{default}' is not a registered markup filter. "
29 | f"Registered filters are: {formatter.registered_filter_names}."
30 | )
31 | raise UnregisteredFilterError(msg)
32 | kwargs.setdefault("default", default)
33 |
34 | kwargs.setdefault("max_length", 255)
35 | kwargs.setdefault("choices", formatter.choices())
36 | kwargs.setdefault("verbose_name", gettext_lazy("markup"))
37 | CharField.__init__(self, *args, **kwargs)
38 |
--------------------------------------------------------------------------------
/django_markup/filter/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 |
4 | class MarkupFilter:
5 | """
6 | Abstract your new filters from this class. This is the most simplest way of
7 | a filter, it accepts the text in it's render method and returns it, as is.
8 | """
9 |
10 | title = "BaseFilter"
11 |
12 | def render(
13 | self,
14 | text: str,
15 | **kwargs: Any, # Unused argument
16 | ) -> str:
17 | return text
18 |
--------------------------------------------------------------------------------
/django_markup/filter/creole_filter.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from django_markup.filter import MarkupFilter
4 |
5 |
6 | class CreoleMarkupFilter(MarkupFilter):
7 | title = "Creole (Wiki Syntax)"
8 |
9 | def render(
10 | self,
11 | text: str,
12 | **kwargs: Any, # Unused argument
13 | ) -> str:
14 | from creole import creole2html
15 |
16 | return creole2html(text)
17 |
--------------------------------------------------------------------------------
/django_markup/filter/linebreaks_filter.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from django.template.defaultfilters import linebreaks
4 |
5 | from django_markup.filter import MarkupFilter
6 |
7 |
8 | class LinebreaksMarkupFilter(MarkupFilter):
9 | """
10 | Replaces line breaks in plain text with appropriate HTML; a single
11 | newline becomes an HTML line break (``
``) and a new line
12 | followed by a blank line becomes a paragraph break (``
Here is some text.
7 |Some rST text.
3 |4 | def test(): 5 | return 'Hello World' 6 |7 |
*This* is some text.
") 9 | 10 | # Simple Markdown 11 | MARKDOWN = ("*This* is some text.", "This is some text.
") 12 | 13 | # Markdown with PRE tag 14 | MARKDOWN_PRE = ( 15 | " code line 1\n code line 2\n", 16 | "code line 1\ncode line 2\n
",
17 | )
18 |
19 | # Simple Markdown
20 | MARKDOWN_JS_LINK = (
21 | '[Javascript Link](javascript:alert("123");)',
22 | '',
23 | )
24 |
25 | # Simple Textile
26 | TEXTILE = (
27 | "*This* is some text.",
28 | "\tThis is some text.
", 29 | ) 30 | 31 | # Simple RestructuredText 32 | RST = ( 33 | "*This* is some text.", 34 | 'This is some text.
\nThis is some text.
", 41 | ) 42 | # Smartypants 43 | SMARTYPANTS = ('This is "some" text.', "This is “some” text.") 44 | 45 | # Windont 46 | WIDONT = ( 47 | "Widont does not leave anyone alone.", 48 | "Widont does not leave anyone alone.", 49 | ) 50 | -------------------------------------------------------------------------------- /django_markup/tests/settings.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | from pathlib import Path 4 | 5 | USE_TZ = False 6 | 7 | SECRET_KEY = "".join(random.sample(string.printable, 20)) 8 | 9 | DATABASES = { 10 | "default": { 11 | "ENGINE": "django.db.backends.sqlite3", 12 | "NAME": "dev.db", 13 | }, 14 | } 15 | 16 | INSTALLED_APPS = [ 17 | "django_markup", 18 | ] 19 | 20 | TEMPLATES = [ 21 | { 22 | "BACKEND": "django.template.backends.django.DjangoTemplates", 23 | "DIRS": [ 24 | Path(__file__).parent / "templates", 25 | ], 26 | "APP_DIRS": True, 27 | "OPTIONS": { 28 | "context_processors": [ 29 | "django.template.context_processors.debug", 30 | "django.template.context_processors.request", 31 | "django.template.context_processors.i18n", 32 | ], 33 | }, 34 | }, 35 | ] 36 | -------------------------------------------------------------------------------- /django_markup/tests/templates/test_templatetag.html: -------------------------------------------------------------------------------- 1 | {% load markup_tags %} 2 | {{ text|apply_markup:filter }} 3 | -------------------------------------------------------------------------------- /django_markup/tests/templates/test_templatetag_filterwrapper.html: -------------------------------------------------------------------------------- 1 | {% load markup_tags %} 2 | {% filter apply_markup:filter %}{{ text }}{% endfilter %} 3 | -------------------------------------------------------------------------------- /django_markup/tests/test_custom_filter.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | 5 | import pytest 6 | from django.test import TestCase 7 | 8 | from django_markup.filter import MarkupFilter 9 | from django_markup.markup import UnregisteredFilterError, formatter 10 | 11 | 12 | class UppercaseMarkupFilter(MarkupFilter): 13 | """ 14 | Custom filter that makes all the text uppercase. 15 | """ 16 | 17 | title = "UppercaseFilter" 18 | 19 | def render( 20 | self, 21 | text: str, 22 | **kwargs: Any, # Unused argument 23 | ) -> str: 24 | return text.upper() 25 | 26 | 27 | class LowercaseMarkupFilter(MarkupFilter): 28 | """ 29 | Custom filter that makes all the text lowercase. 30 | """ 31 | 32 | title = "LowercaseFilter" 33 | 34 | def render( 35 | self, 36 | text: str, 37 | **kwargs: Any, # Unused argument 38 | ) -> str: 39 | return text.lower() 40 | 41 | 42 | class CustomMarkupFilterTestCase(TestCase): 43 | """ 44 | Test the registration/unregistration of a custom filter. 45 | """ 46 | 47 | def test_register_filter(self) -> None: 48 | """ 49 | Register the filter, and its wildly available. 50 | """ 51 | formatter.register("uppercase", UppercaseMarkupFilter) 52 | 53 | # It's ready to be called 54 | result = formatter("This is some text", filter_name="uppercase") 55 | assert result == "THIS IS SOME TEXT" 56 | 57 | def test_update_filter(self) -> None: 58 | """ 59 | You can update an existing filter, but keep the name. 60 | """ 61 | formatter.update("uppercase", LowercaseMarkupFilter) 62 | 63 | # Despite its key name is still 'uppercase' we actually call the 64 | # LowercaseFilter. 65 | result = formatter("This Is Some Text", filter_name="uppercase") 66 | assert result == "this is some text" 67 | 68 | def test_unregister_filter(self) -> None: 69 | # Unregistering a filter that does not exist is simply ignored 70 | formatter.unregister("does-not-exist") 71 | 72 | # Unregistering an registered filter, and it no longer works and will 73 | # raise a ValueError. 74 | formatter.register("uppercase", UppercaseMarkupFilter) 75 | formatter.unregister("uppercase") 76 | 77 | pytest.raises( 78 | UnregisteredFilterError, 79 | formatter, 80 | "This is some text", 81 | filter_name="uppercase", 82 | ) 83 | 84 | def test_fallback_filter(self) -> None: 85 | """ 86 | You can call the formatter without a `filter_name` as long as a 87 | `MARKUP_FILTER_FALLBACK` setting is set. 88 | """ 89 | pytest.raises( 90 | UnregisteredFilterError, 91 | formatter, 92 | "This is some text", 93 | filter_name=None, 94 | ) 95 | 96 | formatter.register("uppercase", UppercaseMarkupFilter) 97 | 98 | with self.settings(MARKUP_FILTER_FALLBACK="uppercase"): 99 | result = formatter("This is some text", filter_name=None) 100 | assert result == "THIS IS SOME TEXT" 101 | -------------------------------------------------------------------------------- /django_markup/tests/test_filter.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | import pytest 6 | from django.test import TestCase 7 | 8 | from django_markup.markup import UnregisteredFilterError, formatter 9 | 10 | from . import markup_strings as s 11 | 12 | FILES_DIR = Path(__file__).parent / "files" 13 | 14 | 15 | class FormatterTestCase(TestCase): 16 | """ 17 | Test the Formatter conversion done in Python of all shipped filters. 18 | """ 19 | 20 | def read(self, filename: str) -> str: 21 | with (FILES_DIR / filename).open("r") as f: 22 | return f.read() 23 | 24 | def test_unregistered_filter_fails_loud(self) -> None: 25 | """ 26 | Trying to call a unregistered filter will raise a ValueError. 27 | """ 28 | pytest.raises( 29 | UnregisteredFilterError, 30 | formatter, 31 | "some text", 32 | filter_name="does-not-exist", 33 | ) 34 | 35 | def test_none_filter(self) -> None: 36 | text, expected = s.NONE 37 | result = formatter(text, filter_name="none") 38 | assert result == expected 39 | 40 | def test_linebreaks_filter(self) -> None: 41 | text, expected = s.LINEBREAKS 42 | result = formatter(text, filter_name="linebreaks") 43 | assert result == expected 44 | 45 | def test_markdown_filter(self) -> None: 46 | text, expected = s.MARKDOWN 47 | result = formatter(text, filter_name="markdown") 48 | assert result == expected 49 | 50 | def test_markdown_filter_pre(self) -> None: 51 | text, expected = s.MARKDOWN_PRE 52 | result = formatter(text, filter_name="markdown") 53 | assert result == expected 54 | 55 | def test_markdown_safemode_enabled_by_default(self) -> None: 56 | """Safe mode is enabled by default.""" 57 | text, expected = s.MARKDOWN_JS_LINK 58 | result = formatter(text, filter_name="markdown") 59 | assert result == expected 60 | 61 | def test_textile_filter(self) -> None: 62 | text, expected = s.TEXTILE 63 | result = formatter(text, filter_name="textile") 64 | assert result == expected 65 | 66 | def test_rst_filter(self) -> None: 67 | text, expected = s.RST 68 | result = formatter(text, filter_name="restructuredtext") 69 | assert result == expected 70 | 71 | def test_rst_header_levels(self) -> None: 72 | """ 73 | Make sure the rST filter fetches the entire document rather just the 74 | document fragment. 75 | 76 | :see: https://github.com/bartTC/django-markup/issues/14 77 | """ 78 | text = self.read("rst_header.txt") 79 | expected = self.read("rst_header_expected.txt") 80 | result = formatter(text, filter_name="restructuredtext") 81 | assert result == expected 82 | 83 | def test_rst_with_pygments(self) -> None: 84 | """ 85 | Having Pygments installed will automatically provide a ``.. code-block`` 86 | directive in reStructredText to highlight code snippets. 87 | """ 88 | text = self.read("rst_with_pygments.txt") 89 | expected = self.read("rst_with_pygments_expected.txt") 90 | result = formatter(text, filter_name="restructuredtext") 91 | 92 | assert result == expected 93 | 94 | def test_rst_raw_default(self) -> None: 95 | """Raw file inclusion is disabled by default.""" 96 | text = self.read("rst_raw.txt") 97 | result = formatter(text, filter_name="restructuredtext") 98 | assert "Other text" in result 99 | assert "