├── .etc └── images │ ├── bootstrap │ ├── martor-editor.png │ ├── martor-guide.png │ └── martor-preview.png │ └── semantic │ ├── martor-editor.png │ ├── martor-guide.png │ └── martor-preview.png ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── release.yml │ └── run-tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── docker-compose.yaml ├── martor ├── __init__.py ├── admin.py ├── api.py ├── extensions │ ├── __init__.py │ ├── del_ins.py │ ├── emoji.py │ ├── escape_html.py │ ├── mdx_add_id.py │ ├── mdx_video.py │ ├── mention.py │ └── urlize.py ├── fields.py ├── locale │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── id │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── tr │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── models.py ├── settings.py ├── static │ ├── dicts │ │ ├── README.md │ │ ├── en_US.aff │ │ └── en_US.dic │ ├── martor │ │ ├── css │ │ │ ├── martor-admin.css │ │ │ ├── martor-admin.min.css │ │ │ ├── martor.bootstrap.css │ │ │ ├── martor.bootstrap.min.css │ │ │ ├── martor.semantic.css │ │ │ └── martor.semantic.min.css │ │ └── js │ │ │ ├── martor.bootstrap.js │ │ │ ├── martor.bootstrap.min.js │ │ │ ├── martor.semantic.js │ │ │ └── martor.semantic.min.js │ └── plugins │ │ ├── css │ │ ├── ace.min.css │ │ ├── bootstrap.min.css │ │ ├── resizable.min.css │ │ └── semantic.min.css │ │ ├── fonts │ │ ├── brand-icons.eot │ │ ├── brand-icons.svg │ │ ├── brand-icons.ttf │ │ ├── brand-icons.woff │ │ ├── brand-icons.woff2 │ │ ├── icons.eot │ │ ├── icons.svg │ │ ├── icons.ttf │ │ ├── icons.woff │ │ ├── icons.woff2 │ │ ├── outline-icons.eot │ │ ├── outline-icons.svg │ │ ├── outline-icons.ttf │ │ ├── outline-icons.woff │ │ └── outline-icons.woff2 │ │ ├── images │ │ ├── commonmark.png │ │ ├── flags.png │ │ └── heart.png │ │ └── js │ │ ├── ace.js │ │ ├── bootstrap.min.js │ │ ├── emojis.min.js │ │ ├── ext-language_tools.js │ │ ├── highlight.min.js │ │ ├── jquery.min.js │ │ ├── jquery.slim.min.js │ │ ├── mode-markdown.js │ │ ├── resizable.min.js │ │ ├── semantic.min.js │ │ ├── snippets │ │ └── markdown.js │ │ ├── spellcheck.js │ │ ├── theme-github.js │ │ └── typo.js ├── templates │ └── martor │ │ ├── bootstrap │ │ ├── editor.html │ │ ├── emoji.html │ │ ├── guide.html │ │ └── toolbar.html │ │ └── semantic │ │ ├── editor.html │ │ ├── emoji.html │ │ ├── guide.html │ │ └── toolbar.html ├── templatetags │ ├── __init__.py │ └── martortags.py ├── tests │ ├── __init__.py │ ├── models.py │ ├── templates │ │ └── test_form_view.html │ ├── tests.py │ ├── urls.py │ └── views.py ├── urls.py ├── utils.py ├── views.py └── widgets.py ├── martor_demo ├── .gitignore ├── app │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ │ ├── .gitignore │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ ├── bootstrap │ │ │ ├── base.html │ │ │ ├── form.html │ │ │ └── test_markdownify.html │ │ └── semantic │ │ │ ├── base.html │ │ │ ├── form.html │ │ │ └── test_markdownify.html │ ├── urls.py │ └── views.py ├── manage.py └── martor_demo │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── push.sh ├── requirements-dev.txt ├── requirements.txt ├── runtests.py ├── setup.cfg └── setup.py /.etc/images/bootstrap/martor-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/5dbc9f1f7e10931472a7309b237c66c2ecb33045/.etc/images/bootstrap/martor-editor.png -------------------------------------------------------------------------------- /.etc/images/bootstrap/martor-guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/5dbc9f1f7e10931472a7309b237c66c2ecb33045/.etc/images/bootstrap/martor-guide.png -------------------------------------------------------------------------------- /.etc/images/bootstrap/martor-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/5dbc9f1f7e10931472a7309b237c66c2ecb33045/.etc/images/bootstrap/martor-preview.png -------------------------------------------------------------------------------- /.etc/images/semantic/martor-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/5dbc9f1f7e10931472a7309b237c66c2ecb33045/.etc/images/semantic/martor-editor.png -------------------------------------------------------------------------------- /.etc/images/semantic/martor-guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/5dbc9f1f7e10931472a7309b237c66c2ecb33045/.etc/images/semantic/martor-guide.png -------------------------------------------------------------------------------- /.etc/images/semantic/martor-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/5dbc9f1f7e10931472a7309b237c66c2ecb33045/.etc/images/semantic/martor-preview.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [agusmakmun] 2 | ko_fi: 3 | liberapay: 4 | issuehunt: 5 | custom: ['https://www.paypal.me/summonagus'] 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report 4 | --- 5 | 6 | 7 | 8 | 9 | ## Details 10 | - OS (Operating System) version: 11 | - Browser and browser version: 12 | - Django version: 13 | - Martor version & theme: 14 | 15 | ### Steps to reproduce 16 | 17 | 1. 18 | 2. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Question 4 | url: https://stackoverflow.com/questions/tagged/martor 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature or enhancement 4 | --- 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | ignore: 8 | # Ignore all patch releases as we can manually 9 | # upgrade if we run into a bug and need a fix. 10 | - dependency-name: "*" 11 | update-types: ["version-update:semver-patch"] 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release and PyPI Upload 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | concurrency: release 12 | permissions: 13 | id-token: write 14 | contents: write 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Python Semantic Release 22 | uses: python-semantic-release/python-semantic-release@master 23 | with: 24 | github_token: '${{ secrets.GH_TOKEN }}' 25 | 26 | upload-to-pypi: 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - name: Set up Python 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: 3.x 36 | 37 | - name: Install dependencies 38 | run: | 39 | python -m pip install --upgrade pip 40 | pip install build 41 | 42 | - name: Build package 43 | run: python -m build 44 | 45 | - name: Publish package 46 | uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc 47 | with: 48 | user: __token__ 49 | password: '${{ secrets.PYPI_API_TOKEN }}' 50 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | # https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | os: [ macos-latest, ubuntu-latest ] 19 | python-version: [ 3.9, "3.10" ] 20 | django-version: [ 3.2, 4.0, 4.1, 4.2 ] 21 | include: 22 | - os: macos-latest 23 | python-version: "3.8" 24 | django-version: "3.2" 25 | - os: ubuntu-latest 26 | python-version: "3.8" 27 | django-version: "3.2" 28 | - os: macos-latest 29 | python-version: "3.8" 30 | django-version: "4.0" 31 | - os: ubuntu-latest 32 | python-version: "3.8" 33 | django-version: "4.0" 34 | - os: macos-latest 35 | python-version: "3.10" 36 | django-version: "5.0" 37 | - os: ubuntu-latest 38 | python-version: "3.10" 39 | django-version: "5.0" 40 | - os: macos-latest 41 | python-version: "3.10" 42 | django-version: "5.0.3" 43 | - os: ubuntu-latest 44 | python-version: "3.10" 45 | django-version: "5.0.3" 46 | 47 | steps: 48 | - name: Checkout code 49 | uses: actions/checkout@v4 50 | 51 | - name: Set up Python ${{ matrix.python-version }} 52 | uses: actions/setup-python@v5 53 | with: 54 | python-version: ${{ matrix.python-version }} 55 | 56 | - name: Install dependencies 57 | run: | 58 | pip install -q Django==${{ matrix.django-version }} 59 | python -m pip install --upgrade pip 60 | python setup.py install 61 | 62 | - name: Test with pytest 63 | run: | 64 | python runtests.py 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build/ 3 | dist/ 4 | venv/ 5 | *.egg-info/ 6 | *.pypirc 7 | *.pyc 8 | *note 9 | *backup* 10 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'docs/|.etc/' 2 | 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.4.0 6 | hooks: 7 | - id: check-yaml 8 | - id: debug-statements 9 | - id: end-of-file-fixer 10 | - id: trailing-whitespace 11 | 12 | - repo: https://github.com/PyCQA/isort 13 | rev: 5.12.0 14 | hooks: 15 | - id: isort 16 | args: ["--profile", "black"] 17 | 18 | - repo: https://github.com/psf/black 19 | rev: 23.7.0 20 | hooks: 21 | - id: black 22 | exclude: tests/test_lowlevel.py 23 | 24 | - repo: https://github.com/asottile/pyupgrade 25 | rev: v3.10.1 26 | hooks: 27 | - id: pyupgrade 28 | args: [--py37-plus] 29 | 30 | - repo: https://github.com/PyCQA/flake8 31 | rev: 6.1.0 32 | hooks: 33 | - id: flake8 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.16.0 2 | 3 | WORKDIR /app 4 | 5 | # Install required packages including tini 6 | RUN set -xe && \ 7 | apk add --no-cache python3 py3-pip tini && \ 8 | pip install --upgrade pip setuptools-scm 9 | 10 | # Copy project files 11 | COPY . . 12 | 13 | # Install Python dependencies and setup Django app 14 | RUN python3 setup.py install && \ 15 | python3 martor_demo/manage.py makemigrations && \ 16 | python3 martor_demo/manage.py migrate 17 | 18 | # Create user and set permissions 19 | RUN addgroup -g 1000 appuser && \ 20 | adduser -u 1000 -G appuser -D -h /app appuser && \ 21 | chown -R appuser:appuser /app 22 | 23 | USER appuser 24 | EXPOSE 8000/tcp 25 | 26 | # Use full path for tini 27 | ENTRYPOINT ["/sbin/tini", "--"] 28 | CMD ["python3", "/app/martor_demo/manage.py", "runserver", "0.0.0.0:8000"] 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include requirements.txt 4 | recursive-include martor/templates * 5 | recursive-include martor/static * 6 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | web: 4 | build: . 5 | ports: 6 | - "8000:8000" 7 | container_name: martor_demo 8 | restart: unless-stopped 9 | -------------------------------------------------------------------------------- /martor/__init__.py: -------------------------------------------------------------------------------- 1 | __VERSION__ = "1.6.45" 2 | __RELEASE_DATE__ = "15-Nov-2024" 3 | __AUTHOR__ = "Agus Makmun (Summon Agus)" 4 | __AUTHOR_EMAIL__ = "summon.agus@gmail.com" 5 | -------------------------------------------------------------------------------- /martor/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import MartorField 4 | from .widgets import AdminMartorWidget 5 | 6 | 7 | class MartorModelAdmin(admin.ModelAdmin): 8 | formfield_overrides = { 9 | MartorField: {"widget": AdminMartorWidget}, 10 | } 11 | -------------------------------------------------------------------------------- /martor/api.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | 4 | import requests 5 | 6 | from .settings import MARTOR_IMGUR_API_KEY, MARTOR_IMGUR_CLIENT_ID 7 | 8 | requests.packages.urllib3.disable_warnings() 9 | 10 | 11 | def imgur_uploader(image): 12 | """ 13 | Basic imgur uploader return as json data. 14 | :param `image` is from `request.FILES['markdown-image-upload']` 15 | :return json response 16 | """ 17 | api_url = "https://api.imgur.com/3/upload.json" 18 | headers = {"Authorization": "Client-ID " + MARTOR_IMGUR_CLIENT_ID} 19 | response = requests.post( 20 | api_url, 21 | headers=headers, 22 | data={ 23 | "key": MARTOR_IMGUR_API_KEY, 24 | "image": base64.b64encode(image.read()), 25 | "type": "base64", 26 | "name": image.name, 27 | }, 28 | ) 29 | 30 | if response.status_code == 200: 31 | response_data = json.loads(response.content.decode("utf-8")) 32 | return json.dumps( 33 | { 34 | "status": response_data["status"], 35 | "link": response_data["data"]["link"], 36 | "name": response_data["data"]["name"], 37 | } 38 | ) 39 | 40 | elif response.status_code == 415: 41 | # Unsupported File type 42 | return json.dumps( 43 | { 44 | "status": response.status_code, 45 | "error": response.reason, 46 | } 47 | ) 48 | 49 | return json.dumps( 50 | { 51 | "status": response.status_code, 52 | "error": response.content.decode("utf-8"), 53 | } 54 | ) 55 | -------------------------------------------------------------------------------- /martor/extensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/5dbc9f1f7e10931472a7309b237c66c2ecb33045/martor/extensions/__init__.py -------------------------------------------------------------------------------- /martor/extensions/del_ins.py: -------------------------------------------------------------------------------- 1 | """ 2 | Del/Ins Extension for Python-Markdown 3 | ===================================== 4 | Wraps the inline content with ins/del tags. 5 | 6 | Usage 7 | ----- 8 | >>> import markdown 9 | >>> src = '''This is ++added content + + and this is ~~deleted content~~''' 10 | >>> html = markdown.markdown(src, ['del_ins']) 11 | >>> print(html) 12 |
This is added content and this is deleted content
13 |
hello summonagus, 18 | i mentioned you!
' 19 | >>> 20 | """ 21 | 22 | MENTION_RE = r"(?>> import markdown 10 | >>> md = markdown.Markdown(extensions=['urlize']) 11 | 12 | >>> md.convert('http://example.com/') 13 | u'' 14 | 15 | >>> md.convert('go to http://example.com') 16 | u'go to http://example.com
' 17 | 18 | >>> md.convert('example.com') 19 | u'' 20 | 21 | >>> md.convert('example.net') 22 | u'' 23 | 24 | >>> md.convert('www.example.us') 25 | u'' 26 | 27 | >>> md.convert('(www.example.us/path/?name=val)') 28 | u'(www.example.us/path/?name=val)
' 29 | 30 | >>> md.convert('go togo to http://example.com now!
' 32 | 33 | Negative examples: 34 | 35 | >>> md.convert('del.icio.us') 36 | u'del.icio.us
' 37 | 38 | """ 39 | 40 | from xml.etree import ElementTree 41 | 42 | import markdown 43 | 44 | # Global Vars 45 | URLIZE_RE = "(%s)" % "|".join( 46 | [ 47 | r"<(?:f|ht)tps?://[^>]*>", 48 | r"\b(?:f|ht)tps?://[^)<>\s]+[^.,)<>\s]", 49 | r"\bwww\.[^)<>\s]+[^.,)<>\s]", 50 | r"[^(<\s]+\.(?:com|net|org)\b", 51 | ] 52 | ) 53 | 54 | 55 | class UrlizePattern(markdown.inlinepatterns.Pattern): 56 | """Return a link Element given an autolink (`http://example/com`).""" 57 | 58 | def handleMatch(self, m): 59 | url = m.group(2) 60 | 61 | if url.startswith("<"): 62 | url = url[1:-1] 63 | 64 | text = url 65 | 66 | if url.split("://")[0] not in ("http", "https", "ftp"): 67 | if "@" in url and "/" not in url: 68 | url = "mailto:" + url 69 | else: 70 | url = "http://" + url 71 | 72 | el = ElementTree.Element("a") 73 | el.set("href", url) 74 | el.text = markdown.util.AtomicString(text) 75 | return el 76 | 77 | 78 | class UrlizeExtension(markdown.Extension): 79 | """Urlize Extension for Python-Markdown.""" 80 | 81 | def extendMarkdown(self, md: markdown.core.Markdown, *args): 82 | """Replace autolink with UrlizePattern""" 83 | md.inlinePatterns.register(UrlizePattern(URLIZE_RE, md), "autolink", 14) 84 | 85 | 86 | def makeExtension(*args, **kwargs): 87 | return UrlizeExtension(*args, **kwargs) 88 | 89 | 90 | if __name__ == "__main__": 91 | import doctest 92 | 93 | doctest.testmod() 94 | -------------------------------------------------------------------------------- /martor/fields.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .settings import MARTOR_ENABLE_LABEL 4 | from .widgets import MartorWidget 5 | 6 | 7 | class MartorFormField(forms.CharField): 8 | def __init__(self, *args, **kwargs): 9 | # to setup the editor without label 10 | if not MARTOR_ENABLE_LABEL: 11 | kwargs["label"] = "" 12 | 13 | super().__init__(*args, **kwargs) 14 | 15 | if not issubclass(self.widget.__class__, MartorWidget): 16 | self.widget = MartorWidget() 17 | -------------------------------------------------------------------------------- /martor/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/5dbc9f1f7e10931472a7309b237c66c2ecb33045/martor/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /martor/locale/en/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR{% trans "Nothing to preview" %}
19 |{% blocktrans with repo_url='https://github.com/agusmakmun/django-markdown-editor' %}Copyright © Martor{% endblocktrans %}
9 |{% blocktrans with doc_url='http://commonmark.org/help/' %}This editor is powered by Markdown. For full 12 | documentation, click here.{% endblocktrans %}
13 |{% trans "Code" %} | 18 |{% trans "Or" %} | 19 |Linux/Windows | 20 |Mac OS | 21 |{% trans "... to Get" %} | 22 |
---|---|---|---|---|
:emoji_name: | 28 |— | 29 |— | 30 |— | 31 |![]() |
32 |
@[username] | 38 |— | 39 |Ctrl+M | 40 |Command+M | 41 |@username | 42 |
48 | | ||||
*Italic* | 53 |_Italic_ | 54 |Ctrl+I | 55 |Command+I | 56 |Italic | 57 |
**Bold** | 60 |__Bold__ | 61 |Ctrl+B | 62 |Command+B | 63 |Bold | 64 |
++Underscores++ | 67 |— | 68 |Ctrl+Shift+U | 69 |Command+Shift+U | 70 |Underscores | 71 |
~~Strikethrough~~ | 74 |— | 75 |Ctrl+Shift+S | 76 |Command+Shift+S | 77 ||
# Heading 1 | 81 |Heading 1 ========= |
82 | Ctrl+Alt+1 | 83 |Command+Option+1 | 84 |
85 | Heading 186 | |
87 |
## Heading 2 | 90 |Heading 2 ----------- |
91 | Ctrl+Alt+2 | 92 |Command+Option+2 | 93 |
94 | Heading 295 | |
96 |
[Link](http://a.com) | 99 |[Link][1] ⁝ [1]: http://b.org |
100 | Ctrl+L | 101 |Command+L | 102 |Link | 103 |
 | 106 |![Image][1] ⁝ [1]: http://url/b.jpg |
107 | Ctrl+Shift+I | 108 |Command+Option+I | 109 |![]() |
110 |
> Blockquote | 113 |— | 114 |Ctrl+Q | 115 |Command+Shift+K | 116 |
117 | Blockquote118 | |
119 |
A paragraph. A paragraph after 1 blank line. |
122 | — | 123 |— | 124 |— | 125 |
126 | A paragraph. 127 |A paragraph after 1 blank line. 128 | |
129 |
132 | * List |
134 |
135 | - List |
137 | Ctrl+U | 138 |Command+U | 139 |
140 |
|
146 |
149 | 1. One |
151 |
152 | 1) One |
154 | Ctrl+Shift+O | 155 |Command+Option+O | 156 |
157 |
|
163 |
Horizontal Rule ----------- |
166 | Horizontal Rule *********** |
167 | Ctrl+H | 168 |Command+H | 169 |Horizontal Rule
170 | 171 | |
172 |
`Inline code` with backticks | 175 |— | 176 |Ctrl+Alt+C | 177 |Command+Option+C | 178 |Inline code with backticks |
179 |
``` def whatever(foo): return foo ``` |
182 | with tab / 4 spaces ....def whatever(foo): .... return foo |
183 | Ctrl+Alt+P | 184 |Command+Option+P | 185 |
186 | def whatever(foo):187 | |
188 |
Advertisement :)
\nxss)
', # noqa: E501 142 | ) 143 | 144 | def test_urls(self): 145 | with override_settings( 146 | MARTOR_MARKDOWNIFY_URL="test/url", 147 | MARTOR_UPLOAD_URL="test/upload", 148 | MARTOR_SEARCH_USERS_URL="test/search", 149 | ): 150 | found = resolve(reverse("martor_markdownfy")) 151 | self.assertEqual(found.func, markdownfy_view) 152 | 153 | found = resolve(reverse("imgur_uploader")) 154 | self.assertEqual(found.func, markdown_imgur_uploader) 155 | 156 | found = resolve(reverse("search_user_json")) 157 | self.assertEqual(found.func, markdown_search_user) 158 | 159 | 160 | class MarkdownifyTest(TestCase): 161 | def test_markdownify_regular_text(self): 162 | markdown_tuples = [ 163 | ("# Heading Level 1", "This is bold text
"), 165 | ("*This is italic text*", "This is italic text
"), 166 | ( 167 | "[Link to google](https://google.com)", 168 | '', 169 | ), 170 | ( 171 | "1. List item 1\n2. List item 2\n3. List item 3", 172 | "[1] - Our Epidemic of Loneliness and Isolation (hhs.gov)
', 200 | ) 201 | 202 | def test_markdownify_not_losing_sentences(self): 203 | markdown_text = "The CDC reports that it was 26.2 per 100,000.[[4]](#ftnt4) In addition, individuals with mental and behavioral health conditions have a higher risk of suicide; rates increased 46 percent from 2000-2020 in non-metro areas.[[5]](#ftnt5)" 204 | 205 | response = markdownify(markdown_text) 206 | # previously, this was 'The CDC reports that it was 26.2 per 100,000.[4]
', dropping the last sentence 207 | self.assertEqual( 208 | response, 209 | 'The CDC reports that it was 26.2 per 100,000.[4] In addition, individuals with mental and behavioral health conditions have a higher risk of suicide; rates increased 46 percent from 2000-2020 in non-metro areas.[5]
', 210 | ) 211 | 212 | def test_markdownify_not_losing_sentences_semicolon(self): 213 | markdown_text = "Citation 4. [footnote 4](#ftnt4) Citation 5. [footnote 5](#ftnt5) Citation 6; [footnote 6](#ftnt6)" 214 | 215 | response = markdownify(markdown_text) 216 | # previously, this was 'Citation 4. footnote 4 Citation 5. footnote 5
' 217 | self.assertEqual( 218 | response, 219 | 'Citation 4. footnote 4 Citation 5. footnote 5 Citation 6; footnote 6
', 220 | ) 221 | 222 | def test_markdownify_not_losing_sentences_colon(self): 223 | markdown_text = "Citation 4. [footnote 4](#ftnt4) Citation 5. [footnote 5](#ftnt5) Citation 6: [footnote 6](#ftnt6)" 224 | 225 | response = markdownify(markdown_text) 226 | # previously, this was 'Citation 4. footnote 4 Citation 5. footnote 5
' 227 | self.assertEqual( 228 | response, 229 | 'Citation 4. footnote 4 Citation 5. footnote 5 Citation 6: footnote 6
', 230 | ) 231 | -------------------------------------------------------------------------------- /martor/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | 3 | from .views import TestFormView 4 | 5 | urlpatterns = [ 6 | path("test-form-view/", TestFormView.as_view()), 7 | path("martor/", include("martor.urls")), 8 | ] 9 | -------------------------------------------------------------------------------- /martor/tests/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic.edit import CreateView 2 | 3 | from .models import Post 4 | 5 | 6 | class TestFormView(CreateView): 7 | template_name = "test_form_view.html" 8 | model = Post 9 | fields = ["description", "wiki"] 10 | -------------------------------------------------------------------------------- /martor/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import markdown_imgur_uploader, markdown_search_user, markdownfy_view 4 | 5 | urlpatterns = [ 6 | path("markdownify/", markdownfy_view, name="martor_markdownfy"), 7 | path("uploader/", markdown_imgur_uploader, name="imgur_uploader"), 8 | path("search-user/", markdown_search_user, name="search_user_json"), 9 | ] 10 | -------------------------------------------------------------------------------- /martor/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import bleach 4 | from django.core.serializers.json import DjangoJSONEncoder 5 | from django.utils.functional import Promise 6 | 7 | try: 8 | from django.utils.encoding import force_str # noqa: Django>=4.x 9 | except ImportError: 10 | from django.utils.encoding import force_text as force_str # noqa: Django<=3.x 11 | 12 | import markdown 13 | 14 | from .settings import ( 15 | ALLOWED_HTML_ATTRIBUTES, 16 | ALLOWED_HTML_TAGS, 17 | ALLOWED_URL_SCHEMES, 18 | MARTOR_MARKDOWN_EXTENSION_CONFIGS, 19 | MARTOR_MARKDOWN_EXTENSIONS, 20 | ) 21 | 22 | 23 | def markdownify(markdown_text): 24 | """ 25 | Render the markdown content to HTML. 26 | 27 | Basic: 28 | >>> from martor.utils import markdownify 29 | >>> content = "" 30 | >>> markdownify(content) 31 | 'Description:
15 |Description:
16 |