├── martor ├── tests │ ├── __init__.py │ ├── urls.py │ ├── views.py │ ├── models.py │ └── templates │ │ └── test_form_view.html ├── extensions │ ├── __init__.py │ ├── escape_html.py │ ├── mdx_add_id.py │ ├── del_ins.py │ ├── mention.py │ ├── urlize.py │ └── mdx_video.py ├── templatetags │ ├── __init__.py │ └── martortags.py ├── static │ ├── plugins │ │ ├── css │ │ │ ├── main-1.png │ │ │ ├── main-13.png │ │ │ ├── main-14.png │ │ │ ├── main-15.png │ │ │ ├── main-16.png │ │ │ ├── main-17.png │ │ │ ├── main-18.png │ │ │ ├── main-19.png │ │ │ ├── main-2.png │ │ │ ├── main-20.png │ │ │ ├── main-21.png │ │ │ ├── main-22.png │ │ │ ├── main-23.png │ │ │ ├── main-24.png │ │ │ ├── main-26.png │ │ │ ├── main-3.png │ │ │ ├── main-4.png │ │ │ ├── main-25.svg │ │ │ ├── main-5.svg │ │ │ ├── main-7.svg │ │ │ ├── main-8.svg │ │ │ ├── main-9.svg │ │ │ ├── highlight.min.css │ │ │ ├── main-6.svg │ │ │ ├── main-12.svg │ │ │ ├── main-10.svg │ │ │ └── main-11.svg │ │ ├── fonts │ │ │ ├── icons.eot │ │ │ ├── icons.ttf │ │ │ ├── icons.woff │ │ │ ├── icons.woff2 │ │ │ ├── brand-icons.eot │ │ │ ├── brand-icons.ttf │ │ │ ├── brand-icons.woff │ │ │ ├── brand-icons.woff2 │ │ │ ├── outline-icons.eot │ │ │ ├── outline-icons.ttf │ │ │ ├── outline-icons.woff │ │ │ └── outline-icons.woff2 │ │ ├── images │ │ │ ├── flags.png │ │ │ ├── heart.png │ │ │ └── commonmark.png │ │ └── js │ │ │ ├── snippets │ │ │ └── markdown.js │ │ │ ├── theme-github.js │ │ │ ├── spellcheck.js │ │ │ └── emojis.min.js │ ├── dicts │ │ ├── README.md │ │ └── en_US.aff │ └── martor │ │ └── css │ │ ├── martor-admin.min.css │ │ ├── martor-admin.css │ │ └── martor.semantic.min.css ├── locale │ ├── en │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── id │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── tr │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── __init__.py ├── admin.py ├── models.py ├── urls.py ├── fields.py ├── templates │ └── martor │ │ ├── semantic │ │ ├── emoji.html │ │ ├── editor.html │ │ ├── toolbar.html │ │ └── guide.html │ │ └── bootstrap │ │ ├── emoji.html │ │ ├── editor.html │ │ └── guide.html ├── api.py ├── utils.py ├── views.py ├── widgets.py └── settings.py ├── martor_demo ├── app │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ └── .gitignore │ ├── apps.py │ ├── forms.py │ ├── urls.py │ ├── models.py │ ├── admin.py │ ├── views.py │ └── templates │ │ └── bootstrap │ │ └── test_markdownify.html ├── .gitignore ├── martor_demo │ ├── __init__.py │ ├── asgi.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py └── manage.py ├── requirements.txt ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── release.yml │ └── run-tests.yml ├── .etc └── images │ ├── semantic │ ├── martor-guide.png │ ├── martor-editor.png │ └── martor-preview.png │ └── bootstrap │ ├── martor-editor.png │ ├── martor-guide.png │ └── martor-preview.png ├── docs ├── requirements.txt ├── Makefile ├── changelog.rst ├── index.rst ├── installation.rst └── conf.py ├── docker-compose.yaml ├── pytest.ini ├── .flake8 ├── .gitignore ├── .readthedocs.yaml ├── .pre-commit-config.yaml ├── Dockerfile ├── runtests.py ├── tests └── collect-static │ ├── README.md │ └── test-collectstatic.sh ├── push.sh └── pyproject.toml /martor/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /martor_demo/app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /martor/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /martor/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /martor_demo/.gitignore: -------------------------------------------------------------------------------- 1 | db.sqlite3 2 | -------------------------------------------------------------------------------- /martor_demo/app/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /martor_demo/martor_demo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /martor_demo/app/migrations/.gitignore: -------------------------------------------------------------------------------- 1 | [^.]* 2 | !__init__.py 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | urllib3 2 | zipp 3 | Django 4 | Markdown 5 | requests 6 | bleach 7 | tzdata 8 | -------------------------------------------------------------------------------- /martor_demo/app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AppConfig(AppConfig): 5 | name = "app" 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [agusmakmun] 2 | ko_fi: 3 | liberapay: 4 | issuehunt: 5 | custom: ['https://www.paypal.me/summonagus'] 6 | -------------------------------------------------------------------------------- /.etc/images/semantic/martor-guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/.etc/images/semantic/martor-guide.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-1.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-13.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-14.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-15.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-16.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-17.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-18.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-19.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-2.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-20.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-21.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-22.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-23.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-24.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-26.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-3.png -------------------------------------------------------------------------------- /martor/static/plugins/css/main-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/css/main-4.png -------------------------------------------------------------------------------- /martor/static/plugins/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/fonts/icons.eot -------------------------------------------------------------------------------- /martor/static/plugins/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/fonts/icons.ttf -------------------------------------------------------------------------------- /.etc/images/bootstrap/martor-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/.etc/images/bootstrap/martor-editor.png -------------------------------------------------------------------------------- /.etc/images/bootstrap/martor-guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/.etc/images/bootstrap/martor-guide.png -------------------------------------------------------------------------------- /.etc/images/semantic/martor-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/.etc/images/semantic/martor-editor.png -------------------------------------------------------------------------------- /.etc/images/semantic/martor-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/.etc/images/semantic/martor-preview.png -------------------------------------------------------------------------------- /martor/locale/en/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/locale/en/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /martor/locale/id/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/locale/id/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /martor/locale/tr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/locale/tr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /martor/static/plugins/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/fonts/icons.woff -------------------------------------------------------------------------------- /martor/static/plugins/fonts/icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/fonts/icons.woff2 -------------------------------------------------------------------------------- /martor/static/plugins/images/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/images/flags.png -------------------------------------------------------------------------------- /martor/static/plugins/images/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/images/heart.png -------------------------------------------------------------------------------- /.etc/images/bootstrap/martor-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/.etc/images/bootstrap/martor-preview.png -------------------------------------------------------------------------------- /martor/static/plugins/fonts/brand-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/fonts/brand-icons.eot -------------------------------------------------------------------------------- /martor/static/plugins/fonts/brand-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/fonts/brand-icons.ttf -------------------------------------------------------------------------------- /martor/static/plugins/fonts/brand-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/fonts/brand-icons.woff -------------------------------------------------------------------------------- /martor/static/plugins/images/commonmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/images/commonmark.png -------------------------------------------------------------------------------- /martor/static/plugins/fonts/brand-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/fonts/brand-icons.woff2 -------------------------------------------------------------------------------- /martor/static/plugins/fonts/outline-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/fonts/outline-icons.eot -------------------------------------------------------------------------------- /martor/static/plugins/fonts/outline-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/fonts/outline-icons.ttf -------------------------------------------------------------------------------- /martor/static/plugins/fonts/outline-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/fonts/outline-icons.woff -------------------------------------------------------------------------------- /martor/static/plugins/fonts/outline-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/HEAD/martor/static/plugins/fonts/outline-icons.woff2 -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=5.0.0 2 | sphinx-rtd-theme>=1.0.0 3 | Django>=3.2 4 | Markdown>=3.0 5 | requests>=2.12.4 6 | bleach 7 | urllib3 8 | zipp 9 | tzdata 10 | -------------------------------------------------------------------------------- /martor/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.7.16" 2 | __release_date__ = "01-Nov-2025" 3 | __author__ = "Agus Makmun (Summon Agus)" 4 | __author_email__ = "summon.agus@gmail.com" 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | DJANGO_SETTINGS_MODULE = martor.tests.settings 3 | python_files = tests.py test_*.py *_tests.py 4 | addopts = -v --tb=short 5 | testpaths = martor/tests 6 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | ignore = E402,E501,W503,W504,E731,E741 4 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,.vscode,build,dist,*.egg-info 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature or enhancement 4 | --- 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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/tests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from martor.models import MartorField 4 | 5 | 6 | class Post(models.Model): 7 | description = MartorField() 8 | wiki = MartorField() 9 | 10 | class Meta: 11 | app_label = "Post" 12 | -------------------------------------------------------------------------------- /martor/static/plugins/css/main-25.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from .fields import MartorFormField 4 | 5 | 6 | class MartorField(models.TextField): 7 | def formfield(self, **kwargs): 8 | defaults = {"form_class": MartorFormField} 9 | defaults.update(kwargs) 10 | return super().formfield(**defaults) 11 | -------------------------------------------------------------------------------- /martor/static/plugins/css/main-5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /martor/static/plugins/css/main-7.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /martor/static/plugins/css/main-8.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /martor_demo/app/forms.py: -------------------------------------------------------------------------------- 1 | from app.models import Post 2 | from django import forms 3 | 4 | from martor.fields import MartorFormField 5 | 6 | 7 | class SimpleForm(forms.Form): 8 | title = forms.CharField(widget=forms.TextInput()) 9 | description = MartorFormField() 10 | wiki = MartorFormField() 11 | 12 | 13 | class PostForm(forms.ModelForm): 14 | class Meta: 15 | model = Post 16 | fields = "__all__" 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build/ 3 | dist/ 4 | venv/ 5 | *.egg-info/ 6 | *.pypirc 7 | *.pyc 8 | *note 9 | *backup* 10 | 11 | # Hatch 12 | .hatch/ 13 | 14 | # Testing and coverage 15 | .pytest_cache/ 16 | .coverage 17 | htmlcov/ 18 | .tox/ 19 | 20 | # Type checking 21 | .mypy_cache/ 22 | 23 | # IDE 24 | .vscode/ 25 | .idea/ 26 | *.swp 27 | *.swo 28 | 29 | # OS 30 | .DS_Store 31 | Thumbs.db 32 | 33 | # Build documentation 34 | docs/_build/ 35 | -------------------------------------------------------------------------------- /martor/extensions/escape_html.py: -------------------------------------------------------------------------------- 1 | import markdown 2 | 3 | 4 | class EscapeHtml(markdown.Extension): 5 | def extendMarkdown(self, md: markdown.core.Markdown, *args): 6 | md.preprocessors.deregister("html_block") 7 | md.inlinePatterns.deregister("html") 8 | 9 | 10 | def makeExtension(*args, **kwargs): 11 | return EscapeHtml(*args, **kwargs) 12 | 13 | 14 | if __name__ == "__main__": 15 | import doctest 16 | 17 | doctest.testmod() 18 | -------------------------------------------------------------------------------- /martor_demo/app/urls.py: -------------------------------------------------------------------------------- 1 | from app.views import ( 2 | home_redirect_view, 3 | post_form_view, 4 | simple_form_view, 5 | test_markdownify, 6 | ) 7 | from django.urls import path 8 | 9 | urlpatterns = [ 10 | path("", home_redirect_view, name="home_redirect"), 11 | path("simple-form/", simple_form_view, name="simple_form"), 12 | path("post-form/", post_form_view, name="post_form"), 13 | path("test-markdownify/", test_markdownify, name="test_markdownify"), 14 | ] 15 | -------------------------------------------------------------------------------- /martor/static/plugins/css/main-9.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /martor_demo/martor_demo/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for martor_demo project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "martor_demo.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /martor_demo/martor_demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for martor_demo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "martor_demo.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version, and other tools you might need 8 | build: 9 | os: ubuntu-24.04 10 | tools: 11 | python: "3.13" 12 | 13 | # Build documentation in the "docs/" directory with Sphinx 14 | sphinx: 15 | configuration: docs/conf.py 16 | 17 | # Python requirements for building documentation 18 | python: 19 | install: 20 | - requirements: docs/requirements.txt 21 | -------------------------------------------------------------------------------- /martor/templatetags/martortags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.safestring import mark_safe 3 | 4 | from ..utils import markdownify 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.filter 10 | def safe_markdown(markdown_text): 11 | """ 12 | Safe the markdown text as html output. 13 | 14 | Usage: 15 | {% load martortags %} 16 | {{ markdown_text|safe_markdown }} 17 | 18 | Example: 19 | {{ post.description|safe_markdown }} 20 | """ 21 | return mark_safe(markdownify(markdown_text)) 22 | -------------------------------------------------------------------------------- /martor/static/dicts/README.md: -------------------------------------------------------------------------------- 1 | The text below is copied from @cfinke's [Typo.js project](https://github.com/cfinke/Typo.js/tree/master/typo/dictionaries/en_US): 2 | 3 | 2006-02-07 release. 4 | 5 | This dictionary is based on a subset of the original English wordlist created by Kevin Atkinson for Pspell and Aspell and thus is covered by his original LGPL license. The affix file is a heavily modified version of the original english.aff file which was released as part of Geoff Kuenning's Ispell and as such is covered by his BSD license. 6 | 7 | Thanks to both authors for their wonderful work. 8 | -------------------------------------------------------------------------------- /martor/templates/martor/semantic/emoji.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 15 | -------------------------------------------------------------------------------- /martor/static/plugins/css/highlight.min.css: -------------------------------------------------------------------------------- 1 | .hljs{display:block;overflow-x:auto;padding:1em;background:white;color:black;border:1px solid #efefef}.hljs-comment,.hljs-quote,.hljs-variable{color:#008000}.hljs-keyword,.hljs-selector-tag,.hljs-built_in,.hljs-name,.hljs-tag{color:#00f}.hljs-string,.hljs-title,.hljs-section,.hljs-attribute,.hljs-literal,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-addition{color:#a31515}.hljs-deletion,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-meta{color:#2b91af}.hljs-doctag{color:#808080}.hljs-attr{color:#f00}.hljs-symbol,.hljs-bullet,.hljs-link{color:#00b0e8}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold} 2 | -------------------------------------------------------------------------------- /martor/static/plugins/css/main-6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /martor_demo/app/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.db import models 3 | 4 | from martor.models import MartorField 5 | 6 | User = get_user_model() 7 | 8 | 9 | class Post(models.Model): 10 | author = models.ForeignKey( 11 | User, 12 | on_delete=models.CASCADE, 13 | blank=True, 14 | null=True, 15 | ) 16 | title = models.CharField(max_length=200) 17 | description = MartorField() 18 | wiki = MartorField(blank=True) 19 | 20 | def __str__(self): 21 | return self.title 22 | 23 | 24 | class PostMeta(models.Model): 25 | post = models.ForeignKey(Post, on_delete=models.CASCADE) 26 | text = MartorField() 27 | 28 | def __str__(self): 29 | return self.text 30 | -------------------------------------------------------------------------------- /martor_demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "martor_demo.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /martor/tests/templates/test_form_view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Martor 5 | 6 | 7 |
8 |
9 |
{% csrf_token %} 10 |
11 | {{ form.description }} 12 |
13 |
14 | {{ form.wiki }} 15 |
16 |
17 |
18 | 21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /martor/static/plugins/css/main-12.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /martor_demo/app/admin.py: -------------------------------------------------------------------------------- 1 | from app.models import Post, PostMeta 2 | from django.contrib import admin 3 | from django.db import models 4 | 5 | from martor.models import MartorField 6 | from martor.widgets import AdminMartorWidget 7 | 8 | 9 | class PostMetaAdminInline(admin.TabularInline): 10 | model = PostMeta 11 | 12 | 13 | class PostMetaStackedInline(admin.StackedInline): 14 | model = PostMeta 15 | 16 | 17 | class PostAdmin(admin.ModelAdmin): 18 | autocomplete_fields = ["author"] 19 | inlines = [PostMetaAdminInline, PostMetaStackedInline] 20 | list_display = ["title", "id"] 21 | formfield_overrides = { 22 | MartorField: {"widget": AdminMartorWidget}, 23 | models.TextField: {"widget": AdminMartorWidget}, 24 | } 25 | 26 | 27 | admin.site.register(Post, PostAdmin) 28 | -------------------------------------------------------------------------------- /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 pip3 install -e . && \ 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 | -------------------------------------------------------------------------------- /martor_demo/martor_demo/urls.py: -------------------------------------------------------------------------------- 1 | """martor_demo URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.conf.urls.static import static 18 | from django.contrib import admin 19 | from django.urls import include, path 20 | 21 | urlpatterns = [ 22 | path("admin/", admin.site.urls), 23 | path("martor/", include("martor.urls")), 24 | path("", include("app.urls")), 25 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 26 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | 22 | # Custom targets for convenience 23 | clean: 24 | @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 25 | 26 | html: 27 | @$(SPHINXBUILD) -b html "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O) 28 | 29 | livehtml: 30 | @sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O) 31 | 32 | install: 33 | @pip install -r requirements.txt 34 | 35 | serve: html 36 | @echo "Serving documentation at http://localhost:8000" 37 | @cd "$(BUILDDIR)/html" && python -m http.server 8000 38 | -------------------------------------------------------------------------------- /martor/extensions/mdx_add_id.py: -------------------------------------------------------------------------------- 1 | import markdown 2 | from xml.etree import ElementTree 3 | 4 | # Regex pattern to detect `{#id_name}` at the end of the line 5 | ADD_ID_RE = r"(.+?)\s\{#([a-zA-Z0-9_-]+)\}$" 6 | 7 | 8 | class AddIDPattern(markdown.inlinepatterns.Pattern): 9 | """Pattern to match Markdown text ending with `{#id}` and set it as an ID.""" 10 | 11 | def handleMatch(self, m): 12 | text_content = m.group(2).strip() # Actual text content 13 | id_value = m.group(3) # The ID inside `{#id}` 14 | 15 | # Create a element to hold the text and ID 16 | el = ElementTree.Element("span") 17 | el.text = markdown.util.AtomicString(text_content) 18 | el.set("id", id_value) 19 | return el 20 | 21 | 22 | class AddIDExtension(markdown.Extension): 23 | """Add ID Extension for Python-Markdown.""" 24 | 25 | def extendMarkdown(self, md: markdown.core.Markdown, *args): 26 | """Register AddIDPattern with the Markdown parser.""" 27 | md.inlinePatterns.register(AddIDPattern(ADD_ID_RE, md), "add_id", 9) 28 | 29 | 30 | def makeExtension(*args, **kwargs): 31 | return AddIDExtension(*args, **kwargs) 32 | 33 | 34 | if __name__ == "__main__": 35 | import doctest 36 | 37 | doctest.testmod() 38 | -------------------------------------------------------------------------------- /martor/static/plugins/css/main-10.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.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 | # environment: Secrets 13 | # permissions: 14 | # id-token: write 15 | # contents: write 16 | 17 | # steps: 18 | # - uses: actions/checkout@v3 19 | # with: 20 | # fetch-depth: 0 21 | 22 | # - name: Python Semantic Release 23 | # uses: python-semantic-release/python-semantic-release@master 24 | # with: 25 | # github_token: '${{ secrets.GH_TOKEN }}' 26 | 27 | upload-to-pypi: 28 | runs-on: ubuntu-latest 29 | environment: Secrets 30 | needs: [] 31 | 32 | steps: 33 | - uses: actions/checkout@v5 34 | 35 | - name: Set up Python 36 | uses: actions/setup-python@v5 37 | with: 38 | python-version: 3.x 39 | 40 | - name: Install dependencies 41 | run: | 42 | python -m pip install --upgrade pip 43 | pip install build 44 | 45 | - name: Build package 46 | run: python -m build 47 | 48 | - name: Publish package 49 | uses: pypa/gh-action-pypi-publish@release/v1 50 | with: 51 | user: __token__ 52 | password: '${{ secrets.PYPI_TOKEN }}' 53 | -------------------------------------------------------------------------------- /martor/templates/martor/semantic/editor.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
3 |
4 | 9 |
10 | 13 |
14 | {{ martor }} 15 | 16 |
17 |
18 |

{% trans "Nothing to preview" %}

19 |
20 |
21 | 22 | {% include 'martor/semantic/guide.html' %} 23 | {% include 'martor/semantic/emoji.html' %} 24 |
25 | -------------------------------------------------------------------------------- /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 |

14 | 15 | Dependencies 16 | ------------ 17 | * [Markdown 2.0+](http://www.freewisdom.org/projects/python-markdown/) 18 | 19 | Copyright 20 | --------- 21 | 2011, 2012 [The active archives contributors](http://activearchives.org/) 22 | All rights reserved. 23 | This software is released under the modified BSD License. 24 | See LICENSE.md for details. 25 | """ 26 | 27 | import markdown 28 | from markdown.inlinepatterns import SimpleTagPattern 29 | 30 | DEL_RE = r"(\~\~)(.+?)(\~\~)" 31 | INS_RE = r"(\+\+)(.+?)(\+\+)" 32 | 33 | 34 | class DelInsExtension(markdown.extensions.Extension): 35 | """Adds del_ins extension to Markdown class.""" 36 | 37 | def extendMarkdown(self, md: markdown.core.Markdown, *args): 38 | del_tag = SimpleTagPattern(DEL_RE, "del") 39 | ins_tag = SimpleTagPattern(INS_RE, "ins") 40 | md.inlinePatterns.register(del_tag, "del", 10) 41 | md.inlinePatterns.register(ins_tag, "ins", 11) 42 | 43 | 44 | def makeExtension(*args, **kwargs): 45 | return DelInsExtension(*args, **kwargs) 46 | 47 | 48 | if __name__ == "__main__": 49 | import doctest 50 | 51 | doctest.testmod() 52 | -------------------------------------------------------------------------------- /martor/templates/martor/bootstrap/emoji.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | python-version: [ "3.10", "3.11", "3.12" ] 19 | django-version: [ "5.0", "5.1", "5.2" ] 20 | include: 21 | # Django 4.2 supports Python 3.8, 3.9, 3.10, 3.11, 3.12 22 | - python-version: "3.8" 23 | django-version: "4.2" 24 | - python-version: "3.9" 25 | django-version: "4.2" 26 | - python-version: "3.10" 27 | django-version: "4.2" 28 | - python-version: "3.11" 29 | django-version: "4.2" 30 | - python-version: "3.12" 31 | django-version: "4.2" 32 | # Python 3.13 only supported in Django 5.1+ 33 | - python-version: "3.13" 34 | django-version: "5.1" 35 | - python-version: "3.13" 36 | django-version: "5.2" 37 | steps: 38 | - name: Checkout code 39 | uses: actions/checkout@v5 40 | 41 | - name: Set up Python ${{ matrix.python-version }} 42 | uses: actions/setup-python@v5 43 | with: 44 | python-version: ${{ matrix.python-version }} 45 | 46 | - name: Install dependencies and run tests 47 | run: | 48 | pip install -U pip setuptools 49 | pip install -q django==${{ matrix.django-version }} 50 | pip install hatch 51 | hatch run test:test 52 | -------------------------------------------------------------------------------- /martor_demo/app/views.py: -------------------------------------------------------------------------------- 1 | from app.forms import PostForm, SimpleForm 2 | from app.models import Post 3 | from django.conf import settings 4 | from django.contrib import messages 5 | from django.contrib.auth.decorators import login_required 6 | from django.shortcuts import redirect, render 7 | 8 | 9 | def home_redirect_view(request): 10 | return redirect("simple_form") 11 | 12 | 13 | def simple_form_view(request): 14 | form = SimpleForm() 15 | context = {"form": form, "title": "Simple Form"} 16 | theme = getattr(settings, "MARTOR_THEME", "bootstrap") 17 | return render(request, "%s/form.html" % theme, context) 18 | 19 | 20 | @login_required 21 | def post_form_view(request): 22 | if request.method == "POST": 23 | form = PostForm(request.POST) 24 | if form.is_valid(): 25 | post = form.save(commit=False) 26 | post.author = request.user 27 | post.save() 28 | messages.success(request, "%s successfully saved." % post.title) 29 | return redirect("test_markdownify") 30 | else: 31 | form = PostForm() 32 | context = {"form": form, "title": "Post Form"} 33 | theme = getattr(settings, "MARTOR_THEME", "bootstrap") 34 | return render(request, "%s/form.html" % theme, context) 35 | 36 | 37 | def test_markdownify(request): 38 | post = Post.objects.last() 39 | context = {"post": post} 40 | if post is None: 41 | context = { 42 | "post": { 43 | "title": "Fake Post", 44 | "description": """It **working**! :heart: [Python Learning](https://python.web.id)""", 45 | } 46 | } 47 | theme = getattr(settings, "MARTOR_THEME", "bootstrap") 48 | return render(request, "%s/test_markdownify.html" % theme, context) 49 | -------------------------------------------------------------------------------- /martor/extensions/mention.py: -------------------------------------------------------------------------------- 1 | from xml.etree import ElementTree 2 | 3 | import markdown 4 | from django.contrib.auth import get_user_model 5 | 6 | from ..settings import MARTOR_ENABLE_CONFIGS, MARTOR_MARKDOWN_BASE_MENTION_URL 7 | 8 | """ 9 | >>> import markdown 10 | >>> md = markdown.Markdown(extensions=['martor.utils.extensions.mention']) 11 | >>> md.convert('@[summonagus]') 12 | '

summonagus

' 14 | >>> 15 | >>> md.convert('hello @[summonagus], i mentioned you!') 16 | '

hello summonagus, 18 | i mentioned you!

' 19 | >>> 20 | """ 21 | 22 | MENTION_RE = r"(?div{width:100%}fieldset .form-row .main-martor{display:grid!important}.submit-row a.deletelink{height:unset}.table.markdown-reference h2{font-size:1.5em;background:none;color:unset;padding:0;font-weight:300!important}.js-inline-admin-formset .form-row:not(.empty-form){display:revert}table{caption-side:top}@media (prefers-color-scheme:dark){body{color:#fff;background:#121212!important}h1,h3,h5{color:#fff!important}.modal-help-guide .modal-content{background:#121212!important}.modal-help-guide .modal-title{color:#333!important;font-size:unset!important;margin:0!important}.modal-help-guide .table.markdown-reference{background:#fff;color:#fff}.modal-help-guide .content{background:#121212!important}.tab-martor-menu .item,.martor-toolbar .ui.basic.buttons .button{color:#fff!important}} 10 | -------------------------------------------------------------------------------- /martor/static/plugins/js/snippets/markdown.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Ace v1.15.3 3 | * https://cdnjs.com/libraries/ace/ 4 | * https://github.com/ajaxorg/ace-builds/blob/v1.15.3/src-min-noconflict/snippets/markdown.js 5 | */ 6 | ace.define("ace/snippets/markdown.snippets",["require","exports","module"],function(e,t,n){n.exports='# Markdown\n\n# Includes octopress (http://octopress.org/) snippets\n\nsnippet [\n [${1:text}](http://${2:address} "${3:title}")\nsnippet [*\n [${1:link}](${2:`@*`} "${3:title}")${4}\n\nsnippet [:\n [${1:id}]: http://${2:url} "${3:title}"\nsnippet [:*\n [${1:id}]: ${2:`@*`} "${3:title}"\n\nsnippet ![\n ![${1:alttext}](${2:/images/image.jpg} "${3:title}")\nsnippet ![*\n ![${1:alt}](${2:`@*`} "${3:title}")${4}\n\nsnippet ![:\n ![${1:id}]: ${2:url} "${3:title}"\nsnippet ![:*\n ![${1:id}]: ${2:`@*`} "${3:title}"\n\nsnippet ===\nregex /^/=+/=*//\n ${PREV_LINE/./=/g}\n \n ${0}\nsnippet ---\nregex /^/-+/-*//\n ${PREV_LINE/./-/g}\n \n ${0}\nsnippet blockquote\n {% blockquote %}\n ${1:quote}\n {% endblockquote %}\n\nsnippet blockquote-author\n {% blockquote ${1:author}, ${2:title} %}\n ${3:quote}\n {% endblockquote %}\n\nsnippet blockquote-link\n {% blockquote ${1:author} ${2:URL} ${3:link_text} %}\n ${4:quote}\n {% endblockquote %}\n\nsnippet bt-codeblock-short\n ```\n ${1:code_snippet}\n ```\n\nsnippet bt-codeblock-full\n ``` ${1:language} ${2:title} ${3:URL} ${4:link_text}\n ${5:code_snippet}\n ```\n\nsnippet codeblock-short\n {% codeblock %}\n ${1:code_snippet}\n {% endcodeblock %}\n\nsnippet codeblock-full\n {% codeblock ${1:title} lang:${2:language} ${3:URL} ${4:link_text} %}\n ${5:code_snippet}\n {% endcodeblock %}\n\nsnippet gist-full\n {% gist ${1:gist_id} ${2:filename} %}\n\nsnippet gist-short\n {% gist ${1:gist_id} %}\n\nsnippet img\n {% img ${1:class} ${2:URL} ${3:width} ${4:height} ${5:title_text} ${6:alt_text} %}\n\nsnippet youtube\n {% youtube ${1:video_id} %}\n\n# The quote should appear only once in the text. It is inherently part of it.\n# See http://octopress.org/docs/plugins/pullquote/ for more info.\n\nsnippet pullquote\n {% pullquote %}\n ${1:text} {" ${2:quote} "} ${3:text}\n {% endpullquote %}\n'}),ace.define("ace/snippets/markdown",["require","exports","module","ace/snippets/markdown.snippets"],function(e,t,n){"use strict";t.snippetText=e("./markdown.snippets"),t.scope="markdown"}); (function() { 7 | ace.require(["ace/snippets/markdown"], function(m) { 8 | if (typeof module == "object" && typeof exports == "object" && module) { 9 | module.exports = m; 10 | } 11 | }); 12 | })(); 13 | -------------------------------------------------------------------------------- /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 = "![awesome](http://i.imgur.com/hvguiSn.jpg)" 30 | >>> markdownify(content) 31 | '

awesome

' 32 | >>> 33 | """ 34 | # Sanitize Markdown links 35 | # https://github.com/netbox-community/netbox/commit/5af2b3c2f577a01d177cb24cda1019551a2a4b64 36 | schemes = "|".join(ALLOWED_URL_SCHEMES) 37 | 38 | # Adjusted pattern to focus on links that do not follow the allowed schemes directly 39 | # The negative lookahead now correctly positioned to ensure it applies to the whole URL 40 | pattern = rf"\[([^\]]+)\]\(((?!({schemes}):)[^)]+)\)" 41 | 42 | markdown_text = re.sub( 43 | pattern, 44 | "[\\1](\\2)", 45 | markdown_text, 46 | flags=re.IGNORECASE, 47 | ) 48 | 49 | html = markdown.markdown( 50 | markdown_text, 51 | extensions=MARTOR_MARKDOWN_EXTENSIONS, 52 | extension_configs=MARTOR_MARKDOWN_EXTENSION_CONFIGS, 53 | ) 54 | return bleach.clean( 55 | html, 56 | tags=ALLOWED_HTML_TAGS, 57 | attributes=ALLOWED_HTML_ATTRIBUTES, 58 | protocols=ALLOWED_URL_SCHEMES, 59 | ) 60 | 61 | 62 | class LazyEncoder(DjangoJSONEncoder): 63 | """ 64 | This problem because we found error encoding, 65 | as docs says, django has special `DjangoJSONEncoder` at 66 | https://docs.djangoproject.com/en/dev/topics/serialization/#serialization-formats-json 67 | also discussed in this answer: http://stackoverflow.com/a/31746279/6396981 68 | 69 | Usage: 70 | >>> data = {} 71 | >>> json.dumps(data, cls=LazyEncoder) 72 | """ 73 | 74 | def default(self, obj): 75 | if isinstance(obj, Promise): 76 | return force_str(obj) 77 | return super().default(obj) 78 | -------------------------------------------------------------------------------- /martor/static/plugins/css/main-11.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /martor/extensions/urlize.py: -------------------------------------------------------------------------------- 1 | """ 2 | Source: https://github.com/r0wb0t/markdown-urlize 3 | 4 | A more liberal autolinker 5 | Inspired by Django's urlize function. 6 | 7 | Positive examples: 8 | 9 | >>> import markdown 10 | >>> md = markdown.Markdown(extensions=['urlize']) 11 | 12 | >>> md.convert('http://example.com/') 13 | u'

http://example.com/

' 14 | 15 | >>> md.convert('go to http://example.com') 16 | u'

go to http://example.com

' 17 | 18 | >>> md.convert('example.com') 19 | u'

example.com

' 20 | 21 | >>> md.convert('example.net') 22 | u'

example.net

' 23 | 24 | >>> md.convert('www.example.us') 25 | u'

www.example.us

' 26 | 27 | >>> md.convert('(www.example.us/path/?name=val)') 28 | u'

(www.example.us/path/?name=val)

' 29 | 30 | >>> md.convert('go to now!') 31 | u'

go 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 | -------------------------------------------------------------------------------- /tests/collect-static/README.md: -------------------------------------------------------------------------------- 1 | # ACE Icons collectstatic Fix - Final Demo 2 | 3 | This directory contains the comprehensive demonstration of the ACE editor icons fix that resolves the `collectstatic` error. 4 | 5 | ## 🎭 Final Demonstration 6 | 7 | The `test-collectstatic.sh` script provides a complete demonstration showing: 8 | 9 | 1. ✅ **Current State Check** - Verifies all 26 icon files are present 10 | 2. ✅ **Initial Test** - Confirms collectstatic works with all files 11 | 3. 🔴 **Error Simulation** - Removes 5 critical icon files to recreate the original error 12 | 4. ❌ **Error Demonstration** - Shows the exact `MissingFileError` that occurs 13 | 5. ✅ **Fix Application** - Restores the missing files 14 | 6. 🎉 **Success Verification** - Confirms collectstatic works with all files present 15 | 16 | ## 🚀 Usage 17 | 18 | ```bash 19 | cd tests/collect-static 20 | ./test-collectstatic.sh 21 | ``` 22 | 23 | ## 📋 Files 24 | 25 | | File | Purpose | 26 | |------|---------| 27 | | `test-collectstatic.sh` | **Complete demonstration script** (error → fix → success) | 28 | | `README.md` | This documentation | 29 | 30 | ## 🎯 What This Demonstrates 31 | 32 | ### ❌ The Original Error 33 | ``` 34 | whitenoise.storage.MissingFileError: The file 'plugins/css/main-1.png' could not be found 35 | 36 | The CSS file 'plugins/css/ace.min.css' references a file which could not be found: 37 | plugins/css/main-1.png 38 | ``` 39 | 40 | ### ✅ The Fix 41 | - Downloaded 26 missing icon files from [ACE builds repository](https://github.com/ajaxorg/ace-builds/tree/v1.37.5/css) 42 | - Files: `main-1.png` through `main-26.png` and `main-5.svg` through `main-25.svg` 43 | - Located in: `martor/static/plugins/css/` 44 | 45 | ### 🔧 Why It Works 46 | 1. **Problem**: `ace.min.css` references icon files that weren't included 47 | 2. **Detection**: WhiteNoise's `CompressedManifestStaticFilesStorage` validates all CSS references 48 | 3. **Solution**: Add the missing files from official ACE repository 49 | 4. **Result**: All references satisfied, `collectstatic` succeeds 50 | 51 | ## 🛠️ Requirements 52 | 53 | - Docker (for build testing) 54 | - Python 3.x with Django 55 | - `whitenoise` package: `pip install whitenoise` 56 | 57 | ## 📊 Expected Output 58 | 59 | The demo will show: 60 | - ✅ **Current state**: 26 icon files found → Docker build succeeds 61 | - ❌ **Error state**: 5 files removed → collectstatic fails with `MissingFileError` 62 | - ✅ **Fixed state**: Files restored → collectstatic succeeds again 63 | 64 | This definitively proves that the missing ACE icon files were the root cause and that our fix permanently resolves the issue. 65 | 66 | ## 🎉 Result 67 | 68 | After running the demo, you'll have concrete proof that: 69 | 1. The original error was caused by missing ACE editor icon files 70 | 2. The fix (adding the 26 icon files) completely resolves the issue 71 | 3. The solution works in both local and Docker environments 72 | 4. The fix is permanent and robust 73 | -------------------------------------------------------------------------------- /martor/templates/martor/bootstrap/editor.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
3 |
4 | 24 | 25 | 41 |
42 | 43 | {% include 'martor/bootstrap/guide.html' %} 44 | {% include 'martor/bootstrap/emoji.html' %} 45 |
46 | -------------------------------------------------------------------------------- /martor/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.auth.decorators import login_required 3 | from django.http import HttpResponse, JsonResponse 4 | from django.utils.module_loading import import_string 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | from .api import imgur_uploader 8 | from .settings import MARTOR_MARKDOWNIFY_FUNCTION 9 | from .utils import LazyEncoder 10 | 11 | User = get_user_model() 12 | 13 | 14 | def markdownfy_view(request): 15 | if request.method == "POST": 16 | content = request.POST.get("content", "") 17 | markdownify = import_string(MARTOR_MARKDOWNIFY_FUNCTION) 18 | return HttpResponse(markdownify(content)) 19 | return HttpResponse(_("Invalid request!")) 20 | 21 | 22 | @login_required 23 | def markdown_imgur_uploader(request): 24 | """ 25 | Makdown image upload for uploading to imgur.com 26 | and represent as json to markdown editor. 27 | """ 28 | 29 | def is_ajax(request): 30 | return request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest" 31 | 32 | if request.method == "POST" and is_ajax(request): 33 | if "markdown-image-upload" in request.FILES: 34 | image = request.FILES["markdown-image-upload"] 35 | response_data = imgur_uploader(image=image) 36 | return HttpResponse(response_data, content_type="application/json") 37 | return HttpResponse(_("Invalid request!")) 38 | return HttpResponse(_("Invalid request!")) 39 | 40 | 41 | @login_required 42 | def markdown_search_user(request): 43 | """ 44 | Json usernames of the users registered & activated. 45 | 46 | url(method=get): 47 | /martor/search-user/?username={username} 48 | 49 | Response: 50 | error: 51 | - `status` is status code (204) 52 | - `error` is error message. 53 | success: 54 | - `status` is status code (204) 55 | - `data` is list dict of usernames. 56 | { 'status': 200, 57 | 'data': [ 58 | {'usernane': 'john'}, 59 | {'usernane': 'albert'}] 60 | } 61 | """ 62 | response_data = {} 63 | username = request.GET.get("username") 64 | 65 | if username is not None and username != "" and " " not in username: 66 | queries = {"%s__icontains" % User.USERNAME_FIELD: username} 67 | users = User.objects.filter(**queries).filter(is_active=True) 68 | if users.exists(): 69 | usernames = list(users.values_list("username", flat=True)) 70 | response_data.update({"status": 200, "data": usernames}) 71 | return JsonResponse(response_data) 72 | 73 | error_message = _( 74 | "No users registered as `%(username)s` " "or user is unactived." 75 | ) 76 | error_message = error_message % {"username": username} 77 | response_data.update({"status": 204, "error": error_message}) 78 | else: 79 | error_message = _("Validation Failed for field `username`") 80 | response_data.update({"status": 204, "error": error_message}) 81 | 82 | return JsonResponse(response_data, encoder=LazyEncoder) 83 | -------------------------------------------------------------------------------- /push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo ''; 4 | echo " a. development"; 5 | echo " b. master"; 6 | echo -n "[+] branch [a/B] ➜ "; 7 | read -r branch; 8 | 9 | echo -n "[+] commit ➜ "; 10 | read -r commit; 11 | 12 | # package folder & name 13 | package=martor 14 | 15 | if [ "$commit" ] && [ "$branch" ]; then 16 | if [ "$branch" == 'b' ] || [ "$branch" == 'B' ]; then 17 | echo -n "[+] upgrade version? [y/N] ➜ "; 18 | read -r upgrade_version; 19 | 20 | if [ "$upgrade_version" == 'y' ] || [ "$upgrade_version" == 'Y' ]; then 21 | # https://stackoverflow.com/a/3250387/6396981 22 | old_version=$(grep '__version__ = ".*"' $package/__init__.py | cut -d '"' -f 2); 23 | old_release_date=$(grep '__release_date__ = ".*"' $package/__init__.py | cut -d '"' -f 2); 24 | echo "[i] current version is $old_version, released at $old_release_date"; 25 | 26 | last_numb_old_version=$(echo "$old_version" | grep -o .$); 27 | last_numb_new_version=$(( last_numb_old_version + 1 )) 28 | 29 | suggested_version="${old_version%?}${last_numb_new_version}" 30 | echo -n "[+] type new version [default:$suggested_version] ➜ "; 31 | read -r input_new_version; 32 | 33 | if [ ! "$input_new_version" ]; then 34 | input_new_version=$suggested_version; 35 | fi 36 | 37 | # 04-Sep-2022 38 | new_release_date=$(date +%d-%b-%Y); 39 | 40 | # https://stackoverflow.com/a/525612/6396981 41 | # sed -i -e => for linux user 42 | # sed -i "" => for mac user 43 | echo "[i] updating new version..." 44 | find $package/ -type f \( -name "*.py" -or -name "*.css" -or -name "*.js" \) -exec sed -i "" "s/$old_version/$input_new_version/g" {} \; 45 | find $package/ -type f \( -name "*.py" -or -name "*.css" -or -name "*.js" \) -exec sed -i "" "s/$old_release_date/$new_release_date/g" {} \; 46 | fi 47 | 48 | git add .; 49 | git commit -m "$commit"; 50 | git checkout master; 51 | git push origin master; 52 | 53 | if [ "$upgrade_version" == 'y' ] || [ "$upgrade_version" == 'Y' ]; then 54 | echo "[i] updating new git tag to v$input_new_version" 55 | git tag -a v"$input_new_version" -m "launch v$input_new_version" 56 | git push origin v"$input_new_version" 57 | 58 | echo "[i] preparing upload to pypi..." 59 | 60 | # Check if hatch is available 61 | if ! [ -x "$(command -v hatch)" ]; then 62 | echo "[!] Error: hatch is not installed or not in PATH" 63 | exit 1 64 | fi 65 | 66 | echo "[i] building package for version $input_new_version..." 67 | hatch build 68 | 69 | # Verify the built package has the correct version 70 | echo "[i] verifying package version..." 71 | if [ -f "dist/$package-$input_new_version.tar.gz" ]; then 72 | echo "[i] package built successfully: $package-$input_new_version.tar.gz" 73 | echo "[i] publishing to PyPI..." 74 | hatch publish 75 | else 76 | echo "[!] Error: Package not found. Expected: dist/$package-$input_new_version.tar.gz" 77 | exit 1 78 | fi 79 | fi 80 | else 81 | git add .; 82 | git commit -m "$commit"; 83 | git checkout development; 84 | git push origin development; 85 | fi 86 | 87 | echo "[i] successfully pushed at $(date)"; 88 | else 89 | echo "[!] not pushed!"; 90 | fi 91 | -------------------------------------------------------------------------------- /martor/static/plugins/js/theme-github.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Ace v1.37.5 3 | * https://cdnjs.com/libraries/ace/ 4 | * https://github.com/ajaxorg/ace-builds/blob/v1.37.5/src-min-noconflict/theme-github.js 5 | */ 6 | ace.define("ace/theme/github-css",["require","exports","module"],function(e,t,n){n.exports='/* CSS style content from github\'s default pygments highlighter template.\n Cursor and selection styles from textmate.css. */\n.ace-github .ace_gutter {\n background: #e8e8e8;\n color: #AAA;\n}\n\n.ace-github {\n background: #fff;\n color: #000;\n}\n\n.ace-github .ace_keyword {\n font-weight: bold;\n}\n\n.ace-github .ace_string {\n color: #D14;\n}\n\n.ace-github .ace_variable.ace_class {\n color: teal;\n}\n\n.ace-github .ace_constant.ace_numeric {\n color: #099;\n}\n\n.ace-github .ace_constant.ace_buildin {\n color: #0086B3;\n}\n\n.ace-github .ace_support.ace_function {\n color: #0086B3;\n}\n\n.ace-github .ace_comment {\n color: #998;\n font-style: italic;\n}\n\n.ace-github .ace_variable.ace_language {\n color: #0086B3;\n}\n\n.ace-github .ace_paren {\n font-weight: bold;\n}\n\n.ace-github .ace_boolean {\n font-weight: bold;\n}\n\n.ace-github .ace_string.ace_regexp {\n color: #009926;\n font-weight: normal;\n}\n\n.ace-github .ace_variable.ace_instance {\n color: teal;\n}\n\n.ace-github .ace_constant.ace_language {\n font-weight: bold;\n}\n\n.ace-github .ace_cursor {\n color: black;\n}\n\n.ace-github.ace_focus .ace_marker-layer .ace_active-line {\n background: rgb(255, 255, 204);\n}\n.ace-github .ace_marker-layer .ace_active-line {\n background: rgb(245, 245, 245);\n}\n\n.ace-github .ace_marker-layer .ace_selection {\n background: rgb(181, 213, 255);\n}\n\n.ace-github.ace_multiselect .ace_selection.ace_start {\n box-shadow: 0 0 3px 0px white;\n}\n/* bold keywords cause cursor issues for some fonts */\n/* this disables bold style for editor and keeps for static highlighter */\n.ace-github.ace_nobold .ace_line > span {\n font-weight: normal !important;\n}\n\n.ace-github .ace_marker-layer .ace_step {\n background: rgb(252, 255, 0);\n}\n\n.ace-github .ace_marker-layer .ace_stack {\n background: rgb(164, 229, 101);\n}\n\n.ace-github .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid rgb(192, 192, 192);\n}\n\n.ace-github .ace_gutter-active-line {\n background-color : rgba(0, 0, 0, 0.07);\n}\n\n.ace-github .ace_marker-layer .ace_selected-word {\n background: rgb(250, 250, 255);\n border: 1px solid rgb(200, 200, 250);\n}\n\n.ace-github .ace_invisible {\n color: #BFBFBF\n}\n\n.ace-github .ace_print-margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-github .ace_indent-guide {\n background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;\n}\n\n.ace-github .ace_indent-guide-active {\n background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAAZSURBVHjaYvj///9/hivKyv8BAAAA//8DACLqBhbvk+/eAAAAAElFTkSuQmCC") right repeat-y;\n}\n'}),ace.define("ace/theme/github",["require","exports","module","ace/theme/github-css","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-github",t.cssText=e("./github-css");var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass,!1)}); (function() { 7 | ace.require(["ace/theme/github"], function(m) { 8 | if (typeof module == "object" && typeof exports == "object" && module) { 9 | module.exports = m; 10 | } 11 | }); 12 | })(); 13 | -------------------------------------------------------------------------------- /martor/static/martor/css/martor-admin.css: -------------------------------------------------------------------------------- 1 | /* 2 | CSS to handle inside admin django 3 | by following the default `admin/css/base.css` from django 4 | */ 5 | 6 | :root { 7 | --primary: #79aec8; 8 | --secondary: #417690; 9 | } 10 | 11 | body { 12 | margin: 0; 13 | padding: 0; 14 | font-size: 14px; 15 | color: #333; 16 | background: #fff; 17 | } 18 | 19 | #container { 20 | -webkit-box-sizing: unset; 21 | box-sizing: unset; 22 | } 23 | 24 | #branding h1 { 25 | margin: 0 !important; 26 | } 27 | 28 | body, 29 | h1, 30 | h2, 31 | h3, 32 | h4, 33 | h5, 34 | button, 35 | input, 36 | optgroup, 37 | select { 38 | font-family: "Roboto", "Lucida Grande", "DejaVu Sans", "Bitstream Vera Sans", Verdana, Arial, sans-serif; 39 | } 40 | 41 | h1, 42 | h2, 43 | h3, 44 | h4, 45 | h5 { 46 | font-weight: bold !important; 47 | line-height: unset !important; 48 | } 49 | 50 | h1 { 51 | margin: 0 0 20px !important; 52 | font-weight: 300 !important; 53 | font-size: 20px !important; 54 | color: #333 !important; 55 | } 56 | 57 | h2 { 58 | font-size: 16px !important; 59 | margin: 1em 0 .5em 0 !important; 60 | } 61 | 62 | h2.subhead { 63 | font-weight: normal !important; 64 | margin-top: 0 !important; 65 | } 66 | 67 | h3 { 68 | font-size: 14px !important; 69 | margin: .8em 0 .3em 0 !important; 70 | color: #333 !important; 71 | font-weight: bold !important; 72 | } 73 | 74 | h4 { 75 | font-size: 12px !important; 76 | margin: 1em 0 .8em 0 !important; 77 | padding-bottom: 3px !important; 78 | } 79 | 80 | h5 { 81 | font-size: 10px !important; 82 | margin: 1.5em 0 .5em 0 !important; 83 | color: #333 !important; 84 | text-transform: uppercase !important; 85 | letter-spacing: 1px !important; 86 | } 87 | 88 | .button, 89 | input[type=submit], 90 | input[type=button], 91 | .submit-row input, 92 | a.button { 93 | padding: 5px 15px; 94 | } 95 | 96 | nav.sticky caption { 97 | caption-side: unset; 98 | } 99 | 100 | .ui.tabular.menu+.attached:not(.top).segment, 101 | .ui.tabular.menu+.attached:not(.top).segment+.attached:not(.top).segment { 102 | width: auto; 103 | } 104 | 105 | fieldset .form-row>div { 106 | width: 100%; 107 | } 108 | 109 | fieldset .form-row .main-martor { 110 | display: grid !important; 111 | } 112 | 113 | .submit-row a.deletelink { 114 | height: unset; 115 | } 116 | 117 | .table.markdown-reference h2 { 118 | font-size: 1.5em; 119 | background: none; 120 | color: unset; 121 | padding: 0; 122 | font-weight: 300 !important; 123 | } 124 | 125 | /* fix issue: #152 */ 126 | .js-inline-admin-formset .form-row:not(.empty-form) { 127 | display: revert; 128 | } 129 | 130 | /* fix issue: #270 */ 131 | table { 132 | caption-side: top; 133 | } 134 | 135 | /* fix issue: #168 */ 136 | @media (prefers-color-scheme: dark) { 137 | body { 138 | color: #fff; 139 | background: #121212 !important; 140 | } 141 | 142 | h1, 143 | h3, 144 | h5 { 145 | color: #fff !important; 146 | } 147 | 148 | /* bootstrap */ 149 | .modal-help-guide .modal-content { 150 | background: #121212 !important; 151 | } 152 | 153 | .modal-help-guide .modal-title { 154 | color: #333 !important; 155 | font-size: unset !important; 156 | margin: 0 !important; 157 | } 158 | 159 | .modal-help-guide .table.markdown-reference { 160 | background: #fff; 161 | color: #fff; 162 | } 163 | 164 | /* semantic-ui */ 165 | .modal-help-guide .content { 166 | background: #121212 !important; 167 | } 168 | 169 | .tab-martor-menu .item, 170 | .martor-toolbar .ui.basic.buttons .button { 171 | color: #fff !important; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | This document tracks the changes made to Martor over time. 5 | 6 | Version 1.7.16 (Current) 7 | ------------------------- 8 | 9 | **Release Date:** 01-Nov-2025 10 | 11 | **Improvements:** 12 | 13 | * Support different CSRF_COOKIE_NAME #286 14 | 15 | Version 1.7.15 16 | ------------------------- 17 | 18 | **Release Date:** 01-Nov-2025 19 | 20 | **New Features:** 21 | 22 | * Complete Sphinx documentation with comprehensive guides 23 | * API reference documentation with autodoc 24 | * Troubleshooting and FAQ sections 25 | * Enhanced code examples and tutorials 26 | 27 | **Improvements:** 28 | 29 | * Better error handling and validation 30 | * Improved accessibility features 31 | * Enhanced security configurations 32 | * Performance optimizations 33 | 34 | **Bug Fixes:** 35 | 36 | * Fixed compatibility issues with Django 5.x 37 | * Resolved static file loading issues 38 | * Fixed admin integration edge cases 39 | * Improved CSRF token handling 40 | * Fixed missing ACE editor icon files causing collectstatic MissingFileError 41 | * Added 26 missing icon files (main-1.png through main-26.png, main-5.svg through main-25.svg) for ACE CSS 42 | 43 | **Documentation:** 44 | 45 | * Complete rewrite of documentation using Sphinx 46 | * Added installation and quickstart guides 47 | * Comprehensive configuration reference 48 | * Usage examples for all components 49 | * API documentation with full coverage 50 | 51 | Previous Versions 52 | ----------------- 53 | 54 | For historical changelog information, see the `GitHub Releases page `_. 55 | 56 | Key Historical Milestones 57 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 58 | 59 | **Version 1.7.x Series** 60 | * Enhanced Django 4.x and 5.x compatibility 61 | * Improved security features 62 | * Better performance and stability 63 | 64 | **Version 1.6.x Series** 65 | * Major UI improvements 66 | * Enhanced mobile support 67 | * Better accessibility 68 | 69 | **Version 1.5.x Series** 70 | * Custom upload support 71 | * Enhanced markdown extensions 72 | * Improved admin integration 73 | 74 | **Version 1.4.x Series** 75 | * Semantic UI theme support 76 | * Performance improvements 77 | * Better customization options 78 | 79 | **Version 1.3.x Series** 80 | * Bootstrap 4 support 81 | * Enhanced security features 82 | * Better Django integration 83 | 84 | **Version 1.2.x Series** 85 | * User mention support 86 | * Improved image handling 87 | * Better form integration 88 | 89 | **Version 1.1.x Series** 90 | * Emoji support 91 | * Enhanced preview functionality 92 | * Better error handling 93 | 94 | **Version 1.0.x Series** 95 | * Initial stable release 96 | * Core markdown editing features 97 | * Basic Django integration 98 | 99 | Migration Guide 100 | --------------- 101 | 102 | From Version 1.6.x to 1.7.x 103 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 104 | 105 | No breaking changes. Simply update the package: 106 | 107 | .. code-block:: bash 108 | 109 | pip install -U martor 110 | 111 | From Earlier Versions 112 | ~~~~~~~~~~~~~~~~~~~~~ 113 | 114 | For major version upgrades, please refer to the specific migration guides in the GitHub repository. 115 | 116 | Deprecation Notices 117 | ------------------- 118 | 119 | **Current Deprecations:** 120 | 121 | * None at this time 122 | 123 | **Planned Deprecations:** 124 | 125 | * Support for Python 3.8 will be dropped in version 1.8.0 126 | * Support for Django 3.1 and earlier will be dropped in version 1.8.0 127 | 128 | Contributing to Changelog 129 | -------------------------- 130 | 131 | When contributing to Martor, please: 132 | 133 | 1. Update this changelog with your changes 134 | 2. Follow the existing format and style 135 | 3. Include the type of change (New Feature, Improvement, Bug Fix, etc.) 136 | 4. Reference any related issues or pull requests 137 | 138 | For more information, see the `Contributing Guide `_. 139 | -------------------------------------------------------------------------------- /martor/static/dicts/en_US.aff: -------------------------------------------------------------------------------- 1 | SET ISO8859-1 2 | TRY esianrtolcdugmphbyfvkwzESIANRTOLCDUGMPHBYFVKWZ' 3 | NOSUGGEST ! 4 | 5 | # ordinal numbers 6 | COMPOUNDMIN 1 7 | # only in compounds: 1th, 2th, 3th 8 | ONLYINCOMPOUND c 9 | # compound rules: 10 | # 1. [0-9]*1[0-9]th (10th, 11th, 12th, 56714th, etc.) 11 | # 2. [0-9]*[02-9](1st|2nd|3rd|[4-9]th) (21st, 22nd, 123rd, 1234th, etc.) 12 | COMPOUNDRULE 2 13 | COMPOUNDRULE n*1t 14 | COMPOUNDRULE n*mp 15 | WORDCHARS 0123456789 16 | 17 | PFX A Y 1 18 | PFX A 0 re . 19 | 20 | PFX I Y 1 21 | PFX I 0 in . 22 | 23 | PFX U Y 1 24 | PFX U 0 un . 25 | 26 | PFX C Y 1 27 | PFX C 0 de . 28 | 29 | PFX E Y 1 30 | PFX E 0 dis . 31 | 32 | PFX F Y 1 33 | PFX F 0 con . 34 | 35 | PFX K Y 1 36 | PFX K 0 pro . 37 | 38 | SFX V N 2 39 | SFX V e ive e 40 | SFX V 0 ive [^e] 41 | 42 | SFX N Y 3 43 | SFX N e ion e 44 | SFX N y ication y 45 | SFX N 0 en [^ey] 46 | 47 | SFX X Y 3 48 | SFX X e ions e 49 | SFX X y ications y 50 | SFX X 0 ens [^ey] 51 | 52 | SFX H N 2 53 | SFX H y ieth y 54 | SFX H 0 th [^y] 55 | 56 | SFX Y Y 1 57 | SFX Y 0 ly . 58 | 59 | SFX G Y 2 60 | SFX G e ing e 61 | SFX G 0 ing [^e] 62 | 63 | SFX J Y 2 64 | SFX J e ings e 65 | SFX J 0 ings [^e] 66 | 67 | SFX D Y 4 68 | SFX D 0 d e 69 | SFX D y ied [^aeiou]y 70 | SFX D 0 ed [^ey] 71 | SFX D 0 ed [aeiou]y 72 | 73 | SFX T N 4 74 | SFX T 0 st e 75 | SFX T y iest [^aeiou]y 76 | SFX T 0 est [aeiou]y 77 | SFX T 0 est [^ey] 78 | 79 | SFX R Y 4 80 | SFX R 0 r e 81 | SFX R y ier [^aeiou]y 82 | SFX R 0 er [aeiou]y 83 | SFX R 0 er [^ey] 84 | 85 | SFX Z Y 4 86 | SFX Z 0 rs e 87 | SFX Z y iers [^aeiou]y 88 | SFX Z 0 ers [aeiou]y 89 | SFX Z 0 ers [^ey] 90 | 91 | SFX S Y 4 92 | SFX S y ies [^aeiou]y 93 | SFX S 0 s [aeiou]y 94 | SFX S 0 es [sxzh] 95 | SFX S 0 s [^sxzhy] 96 | 97 | SFX P Y 3 98 | SFX P y iness [^aeiou]y 99 | SFX P 0 ness [aeiou]y 100 | SFX P 0 ness [^y] 101 | 102 | SFX M Y 1 103 | SFX M 0 's . 104 | 105 | SFX B Y 3 106 | SFX B 0 able [^aeiou] 107 | SFX B 0 able ee 108 | SFX B e able [^aeiou]e 109 | 110 | SFX L Y 1 111 | SFX L 0 ment . 112 | 113 | REP 88 114 | REP a ei 115 | REP ei a 116 | REP a ey 117 | REP ey a 118 | REP ai ie 119 | REP ie ai 120 | REP are air 121 | REP are ear 122 | REP are eir 123 | REP air are 124 | REP air ere 125 | REP ere air 126 | REP ere ear 127 | REP ere eir 128 | REP ear are 129 | REP ear air 130 | REP ear ere 131 | REP eir are 132 | REP eir ere 133 | REP ch te 134 | REP te ch 135 | REP ch ti 136 | REP ti ch 137 | REP ch tu 138 | REP tu ch 139 | REP ch s 140 | REP s ch 141 | REP ch k 142 | REP k ch 143 | REP f ph 144 | REP ph f 145 | REP gh f 146 | REP f gh 147 | REP i igh 148 | REP igh i 149 | REP i uy 150 | REP uy i 151 | REP i ee 152 | REP ee i 153 | REP j di 154 | REP di j 155 | REP j gg 156 | REP gg j 157 | REP j ge 158 | REP ge j 159 | REP s ti 160 | REP ti s 161 | REP s ci 162 | REP ci s 163 | REP k cc 164 | REP cc k 165 | REP k qu 166 | REP qu k 167 | REP kw qu 168 | REP o eau 169 | REP eau o 170 | REP o ew 171 | REP ew o 172 | REP oo ew 173 | REP ew oo 174 | REP ew ui 175 | REP ui ew 176 | REP oo ui 177 | REP ui oo 178 | REP ew u 179 | REP u ew 180 | REP oo u 181 | REP u oo 182 | REP u oe 183 | REP oe u 184 | REP u ieu 185 | REP ieu u 186 | REP ue ew 187 | REP ew ue 188 | REP uff ough 189 | REP oo ieu 190 | REP ieu oo 191 | REP ier ear 192 | REP ear ier 193 | REP ear air 194 | REP air ear 195 | REP w qu 196 | REP qu w 197 | REP z ss 198 | REP ss z 199 | REP shun tion 200 | REP shun sion 201 | REP shun cion 202 | -------------------------------------------------------------------------------- /martor/templates/martor/semantic/toolbar.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
3 |
4 | {% if 'bold' in toolbar_buttons %} 5 |
6 | 7 |
8 | {% endif %} 9 | 10 | {% if 'italic' in toolbar_buttons %} 11 |
12 | 13 |
14 | {% endif %} 15 | 16 | {% if 'horizontal' in toolbar_buttons %} 17 |
18 | 19 |
20 | {% endif %} 21 | 22 | {% if 'heading' in toolbar_buttons %} 23 | 31 | {% endif %} 32 | 33 | {% if 'pre-code' in toolbar_buttons %} 34 | 41 | {% endif %} 42 | 43 | {% if 'blockquote' in toolbar_buttons %} 44 |
45 | 46 |
47 | {% endif %} 48 | 49 | {% if 'unordered-list' in toolbar_buttons %} 50 |
51 | 52 |
53 | {% endif %} 54 | 55 | {% if 'ordered-list' in toolbar_buttons %} 56 |
57 | 58 |
59 | {% endif %} 60 | 61 | {% if 'link' in toolbar_buttons %} 62 | 65 | {% endif %} 66 | 67 | {% if 'image-link' in toolbar_buttons %} 68 | 71 | {% endif %} 72 | 73 | {% if 'image-upload' in toolbar_buttons %} 74 |
75 | 76 | 77 |
78 | {% endif %} 79 | 80 | {% if 'emoji' in toolbar_buttons %} 81 |
82 | 83 |
84 | {% endif %} 85 | 86 | {% if 'direct-mention' in toolbar_buttons %} 87 |
88 | 89 |
90 | {% endif %} 91 | 92 | {% if 'toggle-maximize' in toolbar_buttons %} 93 |
94 | 95 |
96 | {% endif %} 97 | 98 | {% if 'help' in toolbar_buttons %} 99 |
100 | 101 |
102 | {% endif %} 103 |
104 |
105 | -------------------------------------------------------------------------------- /martor_demo/martor_demo/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for martor_demo project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | import tempfile 15 | from pathlib import Path 16 | 17 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 18 | BASE_DIR = Path(__file__).resolve().parent.parent 19 | 20 | 21 | # Quick-start development settings - unsuitable for production 22 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 23 | 24 | # SECURITY WARNING: keep the secret key used in production secret! 25 | SECRET_KEY = "+1zhx_fpkkyj&z+3n!63fx0)og)@h5^7qyr8e0s%c@p8_&t&+l" 26 | 27 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 28 | 29 | # SECURITY WARNING: don't run with debug turned on in production! 30 | DEBUG = True 31 | ALLOWED_HOSTS = ["*"] 32 | 33 | # Martor Configuration 34 | MARTOR_THEME = "bootstrap" # semantic 35 | MARTOR_ENABLE_LABEL = True 36 | MARTOR_ENABLE_CONFIGS = { 37 | "emoji": "true", # to enable/disable emoji icons. 38 | "imgur": "true", # to enable/disable imgur/custom uploader. 39 | "mention": "true", # to enable/disable mention 40 | "jquery": "true", # to include/revoke jquery (require for admin default django) 41 | "living": "false", # to enable/disable live updates in preview 42 | "spellcheck": "false", # to enable/disable spellcheck in form textareas 43 | "hljs": "true", # to enable/disable hljs highlighting in preview 44 | } 45 | MARTOR_TOOLBAR_BUTTONS = [ 46 | "bold", 47 | "italic", 48 | "horizontal", 49 | "heading", 50 | "pre-code", 51 | "blockquote", 52 | "unordered-list", 53 | "ordered-list", 54 | "link", 55 | "image-link", 56 | "image-upload", 57 | "emoji", 58 | "direct-mention", 59 | "toggle-maximize", 60 | "help", 61 | ] 62 | 63 | # Application definition 64 | INSTALLED_APPS = [ 65 | "django.contrib.admin", 66 | "django.contrib.auth", 67 | "django.contrib.contenttypes", 68 | "django.contrib.sessions", 69 | "django.contrib.messages", 70 | "django.contrib.staticfiles", 71 | "martor", 72 | "app", 73 | ] 74 | 75 | MIDDLEWARE = [ 76 | "django.middleware.security.SecurityMiddleware", 77 | "django.contrib.sessions.middleware.SessionMiddleware", 78 | "django.middleware.common.CommonMiddleware", 79 | "django.middleware.csrf.CsrfViewMiddleware", 80 | "django.contrib.auth.middleware.AuthenticationMiddleware", 81 | "django.contrib.messages.middleware.MessageMiddleware", 82 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 83 | ] 84 | 85 | ROOT_URLCONF = "martor_demo.urls" 86 | 87 | TEMPLATES = [ 88 | { 89 | "BACKEND": "django.template.backends.django.DjangoTemplates", 90 | "DIRS": [], 91 | "APP_DIRS": True, 92 | "OPTIONS": { 93 | "context_processors": [ 94 | "django.template.context_processors.debug", 95 | "django.template.context_processors.request", 96 | "django.contrib.auth.context_processors.auth", 97 | "django.contrib.messages.context_processors.messages", 98 | ], 99 | }, 100 | }, 101 | ] 102 | 103 | WSGI_APPLICATION = "martor_demo.wsgi.application" 104 | 105 | 106 | # Database 107 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 108 | 109 | DATABASES = { 110 | "default": { 111 | "ENGINE": "django.db.backends.sqlite3", 112 | "NAME": BASE_DIR / "db.sqlite3", 113 | } 114 | } 115 | 116 | 117 | # Password validation 118 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 119 | 120 | AUTH_PASSWORD_VALIDATORS = [ 121 | { 122 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 123 | }, 124 | { 125 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 126 | }, 127 | { 128 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 129 | }, 130 | { 131 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 132 | }, 133 | ] 134 | 135 | 136 | # Internationalization 137 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 138 | 139 | LANGUAGE_CODE = "en-us" 140 | 141 | TIME_ZONE = "UTC" 142 | 143 | USE_I18N = True 144 | 145 | USE_L10N = True 146 | 147 | USE_TZ = True 148 | 149 | 150 | # Static files (CSS, JavaScript, Images) 151 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 152 | 153 | 154 | STATIC_URL = "/static/" 155 | MEDIA_URL = "/media/" 156 | STATIC_ROOT = os.path.join(tempfile.gettempdir(), "martor_static") 157 | MEDIA_ROOT = os.path.join(tempfile.gettempdir(), "martor_media") 158 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Martor - Django Markdown Editor 2 | =============================== 3 | 4 | .. image:: https://img.shields.io/pypi/v/martor.svg 5 | :target: https://pypi.python.org/pypi/martor 6 | :alt: PyPI Version 7 | 8 | .. image:: https://img.shields.io/badge/license-GNUGPLv3-blue.svg 9 | :target: https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/master/LICENSE 10 | :alt: License 11 | 12 | .. image:: https://img.shields.io/pypi/pyversions/martor.svg 13 | :target: https://pypi.python.org/pypi/martor 14 | :alt: Python Version 15 | 16 | .. image:: https://img.shields.io/badge/Django-3.2%20%3E=%204.2-green.svg 17 | :target: https://www.djangoproject.com 18 | :alt: Django Version 19 | 20 | .. image:: https://img.shields.io/github/actions/workflow/status/agusmakmun/django-markdown-editor/run-tests.yml?branch=master 21 | :target: https://github.com/agusmakmun/django-markdown-editor/actions/workflows/run-tests.yml 22 | :alt: Build Status 23 | 24 | **Martor** is a comprehensive Markdown Editor plugin for Django, designed to provide a powerful and user-friendly markdown editing experience with support for both **Bootstrap** and **Semantic-UI** frameworks. 25 | 26 | Features 27 | -------- 28 | 29 | * 📝 **Live Preview** - Real-time markdown rendering 30 | * ⚡ **Ace Editor Integration** - Powerful code editor with syntax highlighting 31 | * 🎨 **Dual Theme Support** - Bootstrap and Semantic-UI compatible 32 | * 📤 **Image Upload** - Direct upload to imgur.com or custom endpoints 33 | * 👥 **User Mentions** - Direct mention users with ``@[username]`` syntax 34 | * 🎬 **Video Embedding** - Embed videos from YouTube, Vimeo, and more 35 | * ✅ **Spellcheck** - Built-in spell checking (US English) 36 | * 😀 **Emoji Support** - Full emoji support with cheat sheets 37 | * 🔧 **Django Admin Integration** - Seamless admin interface integration 38 | * 🛡️ **Security Features** - XSS protection and HTML sanitization 39 | * 🔗 **Custom ID Support** - Add custom IDs to text elements 40 | * 🧩 **Extensible** - Custom markdown extensions support 41 | 42 | Quick Start 43 | ----------- 44 | 45 | Installation:: 46 | 47 | pip install martor 48 | 49 | Add to your Django settings:: 50 | 51 | INSTALLED_APPS = [ 52 | ... 53 | 'martor', 54 | ] 55 | 56 | Include Martor URLs:: 57 | 58 | urlpatterns = [ 59 | ... 60 | path('martor/', include('martor.urls')), 61 | ] 62 | 63 | Collect static files:: 64 | 65 | python manage.py collectstatic 66 | 67 | Usage in your models:: 68 | 69 | from martor.models import MartorField 70 | 71 | class Post(models.Model): 72 | content = MartorField() 73 | 74 | That's it! You now have a powerful markdown editor in your Django application. 75 | 76 | Documentation Contents 77 | ---------------------- 78 | 79 | .. toctree:: 80 | :maxdepth: 2 81 | :caption: Getting Started 82 | 83 | installation 84 | quickstart 85 | settings 86 | 87 | .. toctree:: 88 | :maxdepth: 2 89 | :caption: User Guide 90 | 91 | usage/models 92 | usage/forms 93 | usage/widgets 94 | usage/admin 95 | usage/templates 96 | 97 | .. toctree:: 98 | :maxdepth: 2 99 | :caption: Advanced Topics 100 | 101 | extensions/index 102 | customization 103 | themes 104 | security 105 | 106 | .. toctree:: 107 | :maxdepth: 2 108 | :caption: API Reference 109 | 110 | api/fields 111 | api/widgets 112 | api/models 113 | api/settings 114 | api/utils 115 | 116 | .. toctree:: 117 | :maxdepth: 2 118 | :caption: Examples & Tutorials 119 | 120 | examples/basic 121 | examples/custom-uploader 122 | examples/mentions 123 | examples/extensions 124 | 125 | .. toctree:: 126 | :maxdepth: 1 127 | :caption: Help & Support 128 | 129 | troubleshooting 130 | faq 131 | changelog 132 | contributing 133 | 134 | Screenshots 135 | ----------- 136 | 137 | Martor Editor Interface: 138 | 139 | .. image:: https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/master/.etc/images/bootstrap/martor-editor.png 140 | :alt: Martor Editor 141 | :align: center 142 | 143 | Live Preview: 144 | 145 | .. image:: https://raw.githubusercontent.com/agusmakmun/django-markdown-editor/master/.etc/images/bootstrap/martor-preview.png 146 | :alt: Martor Preview 147 | :align: center 148 | 149 | Community & Support 150 | ------------------- 151 | 152 | * **GitHub**: https://github.com/agusmakmun/django-markdown-editor 153 | * **PyPI**: https://pypi.org/project/martor/ 154 | * **Issues**: https://github.com/agusmakmun/django-markdown-editor/issues 155 | * **Wiki**: https://github.com/agusmakmun/django-markdown-editor/wiki 156 | 157 | License 158 | ------- 159 | 160 | Martor is released under the `GNU General Public License v3.0 `_. 161 | 162 | Indices and Tables 163 | ================== 164 | 165 | * :ref:`genindex` 166 | * :ref:`modindex` 167 | * :ref:`search` 168 | -------------------------------------------------------------------------------- /martor/static/plugins/js/spellcheck.js: -------------------------------------------------------------------------------- 1 | /* 2 | This code was adapted from the following project: 3 | 4 | https://github.com/swenson/ace_spell_check_js 5 | Copyright (c) 2013 Christopher Swenson 6 | 7 | Code licensed under the MIT Public License: 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | // You also need to load in typo.js and jquery.js 33 | 34 | // You should configure these classes. 35 | var lang = "en_US"; 36 | var dicPath = "/static/dicts/en_US.dic"; 37 | var affPath = "/static/dicts/en_US.aff"; 38 | 39 | // Make red underline for gutter and words. 40 | $("").appendTo("head"); 41 | $("").appendTo("head"); 42 | 43 | // Load the dictionary. 44 | // We have to load the dictionary files sequentially to ensure 45 | var dictionary = null; 46 | $.get(dicPath, function(data) { 47 | dicData = data; 48 | }).done(function() { 49 | $.get(affPath, function(data) { 50 | affData = data; 51 | }).done(function() { 52 | console.log("Dictionary loaded"); 53 | dictionary = new Typo(lang, affData, dicData); 54 | }); 55 | }); 56 | 57 | // Check the spelling of a line, and return [start, end]-pairs for misspelled words. 58 | function misspelled(line) { 59 | var words = line.split(/[^a-zA-Z\-']/); 60 | var i = 0; 61 | var bads = []; 62 | for (word in words) { 63 | var x = words[word] + ""; 64 | var checkWord = x.replace(/[^a-zA-Z\-']/g, ''); 65 | if (!dictionary.check(checkWord)) { 66 | bads[bads.length] = [i, i + words[word].length]; 67 | } 68 | i += words[word].length + 1; 69 | } 70 | return bads; 71 | } 72 | 73 | // Spell check the Ace editor contents. 74 | function spell_check(editorID) { 75 | var editor = ace.edit(editorID); 76 | 77 | // Wait for the dictionary to be loaded. 78 | if (dictionary == null) { 79 | return; 80 | } 81 | 82 | if (editor.currently_spellchecking) { 83 | return; 84 | } 85 | 86 | if (!editor.contents_modified) { 87 | return; 88 | } 89 | editor.currently_spellchecking = true; 90 | var session = editor.getSession(); 91 | 92 | // Clear all markers and gutter 93 | clear_spellcheck_markers(editorID); 94 | // Populate with markers and gutter 95 | try { 96 | var Range = ace.require('ace/range').Range 97 | var lines = session.getDocument().getAllLines(); 98 | for (var i in lines) { 99 | // Check spelling of this line. 100 | var misspellings = misspelled(lines[i]); 101 | 102 | // Add markers and gutter markings. 103 | if (misspellings.length > 0) { 104 | session.addGutterDecoration(i, "misspelled"); 105 | } 106 | 107 | for (var j in misspellings) { 108 | var range = new Range(i, misspellings[j][0], i, misspellings[j][1]); 109 | editor.markers_present[editor.markers_present.length] = session.addMarker(range, "misspelled", "typo", true); 110 | } 111 | } 112 | } finally { 113 | editor.currently_spellchecking = false; 114 | editor.contents_modified = false; 115 | } 116 | } 117 | 118 | function enable_spellcheck(editorID) { 119 | var editor = ace.edit(editorID); 120 | editor.markers_present = []; 121 | editor.spellcheckEnabled = true; 122 | editor.currently_spellchecking = false; 123 | editor.contents_modified = true; 124 | 125 | ace.edit(editor).getSession().on('change', function(e) { 126 | if (editor.spellcheckEnabled) { 127 | editor.contents_modified = true; 128 | spell_check(editorID); 129 | }; 130 | }) 131 | // needed to trigger update once without input 132 | editor.contents_modified = true; 133 | spell_check(editorID); 134 | } 135 | 136 | function disable_spellcheck(editorID) { 137 | var editor = ace.edit(editorID); 138 | editor.spellcheckEnabled = false 139 | 140 | // Clear the markers 141 | clear_spellcheck_markers(editorID); 142 | } 143 | 144 | function clear_spellcheck_markers(editorID) { 145 | var editor = ace.edit(editorID); 146 | var session = editor.getSession(); 147 | 148 | for (var i in editor.markers_present) { 149 | session.removeMarker(editor.markers_present[i]); 150 | }; 151 | 152 | editor.markers_present = []; 153 | 154 | // Clear the gutter 155 | var lines = session.getDocument().getAllLines(); 156 | for (var i in lines) { 157 | session.removeGutterDecoration(i, "misspelled"); 158 | }; 159 | } 160 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "martor" 7 | version = "1.7.16" 8 | description = "Django Markdown Editor" 9 | readme = "README.md" 10 | license = { text = "GPL-3.0" } 11 | authors = [ 12 | { name = "Agus Makmun (Summon Agus)", email = "summon.agus@gmail.com" }, 13 | ] 14 | maintainers = [ 15 | { name = "Agus Makmun (Summon Agus)", email = "summon.agus@gmail.com" }, 16 | ] 17 | keywords = ["martor", "django markdown", "django markdown editor"] 18 | classifiers = [ 19 | "Development Status :: 5 - Production/Stable", 20 | "Environment :: Web Environment", 21 | "Framework :: Django", 22 | "Framework :: Django :: 4.2", 23 | "Framework :: Django :: 5.0", 24 | "Framework :: Django :: 5.1", 25 | "Intended Audience :: Developers", 26 | "Operating System :: OS Independent", 27 | "Programming Language :: JavaScript", 28 | "Programming Language :: Python", 29 | "Programming Language :: Python :: 3.9", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | "Programming Language :: Python :: 3.12", 33 | "Programming Language :: Python :: 3.13", 34 | "Topic :: Software Development :: Libraries :: Python Modules", 35 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 36 | ] 37 | requires-python = ">=3.9" 38 | dependencies = [ 39 | "Django>=3.2", 40 | "Markdown>=3.0", 41 | "requests>=2.12.4", 42 | "bleach", 43 | "urllib3", 44 | "zipp", 45 | "tzdata", 46 | ] 47 | 48 | [project.urls] 49 | Homepage = "https://github.com/agusmakmun/django-markdown-editor" 50 | Documentation = "https://github.com/agusmakmun/django-markdown-editor?tab=readme-ov-file#installation" 51 | Release-notes = "https://github.com/agusmakmun/django-markdown-editor/releases" 52 | Funding = "https://www.paypal.com/paypalme/summonagus" 53 | Source = "https://github.com/agusmakmun/django-markdown-editor" 54 | Issues = "https://github.com/agusmakmun/django-markdown-editor/issues" 55 | Download = "https://github.com/agusmakmun/django-markdown-editor/releases" 56 | 57 | [project.optional-dependencies] 58 | dev = [ 59 | "isort", 60 | "black", 61 | "flake8", 62 | "mypy", 63 | "mypy-extensions", 64 | "django-stubs", 65 | "pytest", 66 | "pytest-django", 67 | ] 68 | 69 | 70 | [tool.hatch.build.targets.wheel] 71 | packages = ["martor"] 72 | include = ["/martor/__init__.py"] 73 | 74 | [tool.hatch.build.targets.sdist] 75 | include = [ 76 | "/LICENSE", 77 | "/README.md", 78 | "/requirements.txt", 79 | "/martor", 80 | "/martor/templates", 81 | "/martor/static", 82 | "/martor/__init__.py", 83 | ] 84 | 85 | # Testing configuration 86 | [tool.hatch.envs.test] 87 | dependencies = [ 88 | "pytest", 89 | "pytest-django", 90 | "Django>=3.2", 91 | "Markdown>=3.0", 92 | "requests>=2.12.4", 93 | "bleach", 94 | ] 95 | 96 | [tool.hatch.envs.test.scripts] 97 | test = "python runtests.py" 98 | test-pytest = "pytest" 99 | test-verbose = "python runtests.py -v" 100 | 101 | # Development environment 102 | [tool.hatch.envs.dev] 103 | dependencies = [ 104 | "isort", 105 | "black", 106 | "flake8", 107 | "mypy", 108 | "mypy-extensions", 109 | "django-stubs", 110 | ] 111 | 112 | [tool.hatch.envs.dev.scripts] 113 | format = "black ." 114 | format-check = "black --check ." 115 | lint = "flake8 ." 116 | sort = "isort ." 117 | sort-check = "isort --check-only ." 118 | type-check = "mypy ." 119 | 120 | # Code formatting 121 | [tool.black] 122 | line-length = 120 123 | target-version = ['py39'] 124 | include = '\.pyi?$' 125 | extend-exclude = ''' 126 | /( 127 | # directories 128 | \.eggs 129 | | \.git 130 | | \.hg 131 | | \.mypy_cache 132 | | \.tox 133 | | \.venv 134 | | build 135 | | dist 136 | )/ 137 | ''' 138 | 139 | # Import sorting 140 | [tool.isort] 141 | profile = "black" 142 | line_length = 120 143 | multi_line_output = 3 144 | include_trailing_comma = true 145 | force_grid_wrap = 0 146 | use_parentheses = true 147 | ensure_newline_before_comments = true 148 | 149 | # Linting 150 | [tool.flake8] 151 | max-line-length = 120 152 | ignore = ["E402", "E501", "W503", "W504", "E731", "E741"] 153 | exclude = [ 154 | ".tox", 155 | ".git", 156 | "*/migrations/*", 157 | "*/static/CACHE/*", 158 | "docs", 159 | "node_modules", 160 | ".vscode", 161 | "build", 162 | "dist", 163 | "*.egg-info", 164 | ] 165 | 166 | # Type checking 167 | [tool.mypy] 168 | python_version = "3.9" 169 | check_untyped_defs = false 170 | ignore_missing_imports = true 171 | warn_unused_ignores = false 172 | warn_redundant_casts = false 173 | warn_unused_configs = false 174 | # Temporarily disable Django plugin until settings module is created 175 | # plugins = ["mypy_django_plugin.main"] 176 | 177 | [[tool.mypy.overrides]] 178 | module = "*.migrations.*" 179 | ignore_errors = true 180 | 181 | [[tool.mypy.overrides]] 182 | module = "martor.*" 183 | ignore_errors = true 184 | 185 | [[tool.mypy.overrides]] 186 | module = "martor_demo.*" 187 | ignore_errors = true 188 | 189 | [[tool.mypy.overrides]] 190 | module = "martor_demo.app.*" 191 | ignore_errors = true 192 | 193 | # Django stubs configuration (commented out until settings module is created) 194 | # [tool.django-stubs] 195 | # django_settings_module = "martor.tests.settings" 196 | # strict_settings = false 197 | 198 | # Codespell 199 | [tool.codespell] 200 | skip = "*.po,,*/static,*.png,*.jpg,*.jpeg,*.svg,*.ico,*.zip,*.pdf,*.egg-info,*/build" 201 | ignore-words-list = "delink,mimicing,beforeall,afterall,ninjs,womens" 202 | count = true 203 | quiet-level = 3 204 | -------------------------------------------------------------------------------- /martor/widgets.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | from django import forms 5 | from django.contrib.admin import widgets 6 | from django.template.loader import get_template 7 | from django.urls import reverse 8 | 9 | from .settings import ( 10 | MARTOR_ALTERNATIVE_CSS_FILE_THEME, 11 | MARTOR_ALTERNATIVE_JQUERY_JS_FILE, 12 | MARTOR_ALTERNATIVE_JS_FILE_THEME, 13 | MARTOR_ENABLE_ADMIN_CSS, 14 | MARTOR_ENABLE_CONFIGS, 15 | MARTOR_MARKDOWN_BASE_EMOJI_URL, 16 | MARTOR_MARKDOWNIFY_TIMEOUT, 17 | MARTOR_SEARCH_USERS_URL, 18 | MARTOR_THEME, 19 | MARTOR_TOOLBAR_BUTTONS, 20 | MARTOR_UPLOAD_URL, 21 | MARTOR_CSRF_COOKIE_NAME, 22 | ) 23 | 24 | 25 | def get_theme(): 26 | """function to get the selected theme""" 27 | supported_themes = ["bootstrap", "semantic"] 28 | if MARTOR_THEME in supported_themes: 29 | return MARTOR_THEME 30 | return "bootstrap" 31 | 32 | 33 | class MartorWidget(forms.Textarea): 34 | def render(self, name, value, attrs=None, renderer=None, **kwargs): 35 | # Create random string to make field ID unique to prevent duplicated ID 36 | # when rendering fields with the same field name 37 | random_string = "".join(random.choice(string.ascii_letters + string.digits) for x in range(10)) 38 | attrs["id"] = attrs["id"] + "-" + random_string 39 | 40 | # Make the settings the default attributes to pass 41 | attributes_to_pass = { 42 | "data-enable-configs": MARTOR_ENABLE_CONFIGS, 43 | "data-markdownfy-url": reverse("martor_markdownfy"), 44 | "data-csrf-cookie-name": MARTOR_CSRF_COOKIE_NAME, 45 | } 46 | 47 | if MARTOR_UPLOAD_URL: 48 | attributes_to_pass["data-upload-url"] = MARTOR_UPLOAD_URL 49 | if MARTOR_SEARCH_USERS_URL: 50 | attributes_to_pass["data-search-users-url"] = MARTOR_SEARCH_USERS_URL 51 | if MARTOR_MARKDOWN_BASE_EMOJI_URL: 52 | attributes_to_pass["data-base-emoji-url"] = MARTOR_MARKDOWN_BASE_EMOJI_URL 53 | if MARTOR_MARKDOWNIFY_TIMEOUT: 54 | attributes_to_pass["data-save-timeout"] = MARTOR_MARKDOWNIFY_TIMEOUT 55 | 56 | 57 | # Make sure that the martor value is in the class attr passed in 58 | if "class" in attrs: 59 | attrs["class"] += " martor" 60 | else: 61 | attrs["class"] = "martor" 62 | 63 | # Update and overwrite with the attributes passed in 64 | attributes_to_pass.update(attrs) 65 | 66 | # Update and overwrite with any attributes that are on the widget 67 | # itself. This is also the only way we can push something in without 68 | # being part of the render chain. 69 | attributes_to_pass.update(self.attrs) 70 | 71 | template = get_template("martor/%s/editor.html" % get_theme()) 72 | emoji_enabled = MARTOR_ENABLE_CONFIGS.get("emoji") == "true" 73 | mentions_enabled = MARTOR_ENABLE_CONFIGS.get("mention") == "true" 74 | 75 | widget = super().render(name, value, attributes_to_pass) 76 | 77 | return template.render( 78 | { 79 | "martor": widget, 80 | "field_name": name + "-" + random_string, 81 | "emoji_enabled": emoji_enabled, 82 | "mentions_enabled": mentions_enabled, 83 | "toolbar_buttons": MARTOR_TOOLBAR_BUTTONS, 84 | } 85 | ) 86 | 87 | class Media: 88 | selected_theme = get_theme() 89 | css = { 90 | "all": ( 91 | "plugins/css/ace.min.css", 92 | "plugins/css/highlight.min.css", 93 | "martor/css/martor.%s.min.css" % selected_theme, 94 | ) 95 | } 96 | 97 | if MARTOR_ENABLE_ADMIN_CSS: 98 | admin_theme = ("martor/css/martor-admin.min.css",) 99 | css["all"] = admin_theme.__add__(css.get("all")) 100 | 101 | js = ( 102 | "plugins/js/ace.js", 103 | "plugins/js/mode-markdown.js", 104 | "plugins/js/ext-language_tools.js", 105 | "plugins/js/theme-github.js", 106 | "plugins/js/highlight.min.js", 107 | "plugins/js/emojis.min.js", 108 | "martor/js/martor.%s.min.js" % selected_theme, 109 | ) 110 | 111 | # Adding the following scripts to the end 112 | # of the tuple in case it affects behaviour. 113 | # spellcheck configuration 114 | if MARTOR_ENABLE_CONFIGS.get("spellcheck") == "true": 115 | js = ("plugins/js/typo.js", "plugins/js/spellcheck.js").__add__(js) 116 | 117 | # support alternative vendor theme file like: bootstrap, semantic) 118 | # 1. vendor css theme 119 | if MARTOR_ALTERNATIVE_CSS_FILE_THEME: 120 | css_theme = MARTOR_ALTERNATIVE_CSS_FILE_THEME 121 | css["all"] = (css_theme,).__add__(css.get("all")) 122 | else: 123 | css_theme = "plugins/css/%s.min.css" % selected_theme 124 | css["all"] = (css_theme,).__add__(css.get("all")) 125 | 126 | # 2. vendor js theme 127 | if MARTOR_ALTERNATIVE_JS_FILE_THEME: 128 | js_theme = MARTOR_ALTERNATIVE_JS_FILE_THEME 129 | js = (MARTOR_ALTERNATIVE_JS_FILE_THEME,).__add__(js) 130 | else: 131 | js_theme = "plugins/js/%s.min.js" % selected_theme 132 | js = (js_theme,).__add__(js) 133 | 134 | # 3. vendor jQUery 135 | if MARTOR_ALTERNATIVE_JQUERY_JS_FILE: 136 | js = (MARTOR_ALTERNATIVE_JQUERY_JS_FILE,).__add__(js) 137 | elif MARTOR_ENABLE_CONFIGS.get("jquery") == "true": 138 | js = ("plugins/js/jquery.min.js",).__add__(js) 139 | 140 | 141 | class AdminMartorWidget(MartorWidget, widgets.AdminTextareaWidget): 142 | pass 143 | -------------------------------------------------------------------------------- /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 , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2025-09-03 23:53+0700\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: templates/martor/bootstrap/editor.html:11 21 | #: templates/martor/semantic/editor.html:5 22 | msgid "Editor" 23 | msgstr "" 24 | 25 | #: templates/martor/bootstrap/editor.html:18 26 | #: templates/martor/semantic/editor.html:6 27 | msgid "Preview" 28 | msgstr "" 29 | 30 | #: templates/martor/bootstrap/editor.html:31 31 | #: templates/martor/semantic/editor.html:11 32 | msgid "Uploading... please wait..." 33 | msgstr "" 34 | 35 | #: templates/martor/bootstrap/editor.html:38 36 | #: templates/martor/semantic/editor.html:18 37 | msgid "Nothing to preview" 38 | msgstr "" 39 | 40 | #: templates/martor/bootstrap/emoji.html:13 41 | #: templates/martor/semantic/emoji.html:4 42 | msgid "Select Emoji to Insert" 43 | msgstr "" 44 | 45 | #: templates/martor/bootstrap/emoji.html:22 46 | #: templates/martor/semantic/emoji.html:8 47 | msgid "Preparing emojis..." 48 | msgstr "" 49 | 50 | #: templates/martor/bootstrap/guide.html:14 51 | #: templates/martor/semantic/guide.html:4 52 | msgid "Markdown Guide" 53 | msgstr "" 54 | 55 | #: templates/martor/bootstrap/guide.html:20 56 | #, python-format 57 | msgid "" 58 | "Copyright © Martor" 60 | msgstr "" 61 | 62 | #: templates/martor/bootstrap/guide.html:24 63 | #, python-format 64 | msgid "" 65 | "This editor is powered by Markdown. For full\n" 66 | " documentation, click " 67 | "here." 68 | msgstr "" 69 | 70 | #: templates/martor/bootstrap/guide.html:30 71 | #: templates/martor/bootstrap/toolbar.html:53 72 | #: templates/martor/semantic/guide.html:17 73 | #: templates/martor/semantic/toolbar.html:38 74 | msgid "Code" 75 | msgstr "" 76 | 77 | #: templates/martor/bootstrap/guide.html:31 78 | #: templates/martor/semantic/guide.html:18 79 | msgid "Or" 80 | msgstr "" 81 | 82 | #: templates/martor/bootstrap/guide.html:34 83 | #: templates/martor/semantic/guide.html:21 84 | msgid "... to Get" 85 | msgstr "" 86 | 87 | #: templates/martor/bootstrap/toolbar.html:3 88 | msgid "Martor Toolbar Buttons" 89 | msgstr "" 90 | 91 | #: templates/martor/bootstrap/toolbar.html:6 92 | #: templates/martor/semantic/toolbar.html:5 93 | msgid "Bold" 94 | msgstr "" 95 | 96 | #: templates/martor/bootstrap/toolbar.html:14 97 | #: templates/martor/semantic/toolbar.html:11 98 | msgid "Italic" 99 | msgstr "" 100 | 101 | #: templates/martor/bootstrap/toolbar.html:22 102 | #: templates/martor/semantic/toolbar.html:17 103 | msgid "Horizontal Line" 104 | msgstr "" 105 | 106 | #: templates/martor/bootstrap/toolbar.html:31 107 | #: templates/martor/bootstrap/toolbar.html:37 108 | #: templates/martor/bootstrap/toolbar.html:38 109 | #: templates/martor/bootstrap/toolbar.html:39 110 | #: templates/martor/semantic/toolbar.html:23 111 | #: templates/martor/semantic/toolbar.html:26 112 | #: templates/martor/semantic/toolbar.html:27 113 | #: templates/martor/semantic/toolbar.html:28 114 | msgid "Heading" 115 | msgstr "" 116 | 117 | #: templates/martor/bootstrap/toolbar.html:46 118 | #: templates/martor/semantic/toolbar.html:34 119 | msgid "Pre or Code" 120 | msgstr "" 121 | 122 | #: templates/martor/bootstrap/toolbar.html:52 123 | #: templates/martor/semantic/toolbar.html:37 124 | msgid "Pre" 125 | msgstr "" 126 | 127 | #: templates/martor/bootstrap/toolbar.html:59 128 | #: templates/martor/semantic/toolbar.html:44 129 | msgid "Quote" 130 | msgstr "" 131 | 132 | #: templates/martor/bootstrap/toolbar.html:68 133 | #: templates/martor/semantic/toolbar.html:50 134 | msgid "Unordered List" 135 | msgstr "" 136 | 137 | #: templates/martor/bootstrap/toolbar.html:76 138 | #: templates/martor/semantic/toolbar.html:56 139 | msgid "Ordered List" 140 | msgstr "" 141 | 142 | #: templates/martor/bootstrap/toolbar.html:85 143 | #: templates/martor/semantic/toolbar.html:62 144 | msgid "URL/Link" 145 | msgstr "" 146 | 147 | #: templates/martor/bootstrap/toolbar.html:94 148 | #: templates/martor/semantic/toolbar.html:68 149 | msgid "Insert Image Link" 150 | msgstr "" 151 | 152 | #: templates/martor/bootstrap/toolbar.html:102 153 | #: templates/martor/bootstrap/toolbar.html:107 154 | #: templates/martor/semantic/toolbar.html:74 155 | #: templates/martor/semantic/toolbar.html:76 156 | msgid "Upload an Image" 157 | msgstr "" 158 | 159 | #: templates/martor/bootstrap/toolbar.html:112 160 | #: templates/martor/semantic/toolbar.html:81 161 | msgid "Insert Emoji" 162 | msgstr "" 163 | 164 | #: templates/martor/bootstrap/toolbar.html:120 165 | #: templates/martor/semantic/toolbar.html:87 166 | msgid "Direct Mention User" 167 | msgstr "" 168 | 169 | #: templates/martor/bootstrap/toolbar.html:128 170 | #: templates/martor/semantic/toolbar.html:93 171 | msgid "Full Screen" 172 | msgstr "" 173 | 174 | #: templates/martor/bootstrap/toolbar.html:139 175 | #: templates/martor/semantic/toolbar.html:99 176 | msgid "Markdown Guide (Help)" 177 | msgstr "" 178 | 179 | #: templates/martor/semantic/guide.html:7 180 | #, python-format 181 | msgid "" 182 | "Copyright © Martor" 184 | msgstr "" 185 | 186 | #: templates/martor/semantic/guide.html:11 187 | #, python-format 188 | msgid "" 189 | "This editor is powered by Markdown. For full\n" 190 | " documentation, click here." 192 | msgstr "" 193 | 194 | #: views.py:19 views.py:37 views.py:38 195 | msgid "Invalid request!" 196 | msgstr "" 197 | 198 | #: views.py:74 199 | #, python-format 200 | msgid "No users registered as `%(username)s` or user is unactived." 201 | msgstr "" 202 | 203 | #: views.py:79 204 | msgid "Validation Failed for field `username`" 205 | msgstr "" 206 | -------------------------------------------------------------------------------- /martor/extensions/mdx_video.py: -------------------------------------------------------------------------------- 1 | from xml.etree import ElementTree 2 | 3 | import markdown 4 | 5 | 6 | class VideoExtension(markdown.Extension): 7 | def __init__(self, **kwargs): 8 | self.config = { 9 | "dailymotion_width": ["480", "Width for Dailymotion videos"], 10 | "dailymotion_height": ["270", "Height for Dailymotion videos"], 11 | "metacafe_width": ["440", "Width for Metacafe videos"], 12 | "metacafe_height": ["248", "Height for Metacafe videos"], 13 | "veoh_width": ["410", "Width for Veoh videos"], 14 | "veoh_height": ["341", "Height for Veoh videos"], 15 | "vimeo_width": ["500", "Width for Vimeo videos"], 16 | "vimeo_height": ["281", "Height for Vimeo videos"], 17 | "yahoo_width": ["624", "Width for Yahoo! videos"], 18 | "yahoo_height": ["351", "Height for Yahoo! videos"], 19 | "youtube_width": ["560", "Width for Youtube videos"], 20 | "youtube_height": ["315", "Height for Youtube videos"], 21 | "youtube_nocookie": [ 22 | False, 23 | "Use youtube-nocookie.com instead of youtube.com", 24 | ], 25 | } 26 | 27 | # Override defaults with user settings 28 | for key, value in kwargs.items(): 29 | self.setConfig(key, str(value)) 30 | 31 | def add_inline(self, md: markdown.core.Markdown, name: str, klass: type, re: str): 32 | pattern = klass(re) 33 | pattern.md = md 34 | pattern.ext = self 35 | md.inlinePatterns.register(pattern, name, 15) 36 | 37 | def extendMarkdown(self, md, *args): 38 | self.add_inline( 39 | md, 40 | "dailymotion", 41 | Dailymotion, 42 | r"([^(]|^)https?://www\.dailymotion\.com/video/(?P[a-zA-Z0-9]+)(_[\w\-]*)?", # noqa: E501 43 | ) 44 | self.add_inline( 45 | md, 46 | "metacafe", 47 | Metacafe, 48 | r"([^(]|^)http://www\.metacafe\.com/watch/(?P\d+)/?(:?.+/?)", # noqa: E501 49 | ) 50 | self.add_inline( 51 | md, 52 | "veoh", 53 | Veoh, 54 | r"([^(]|^)http://www\.veoh\.com/\S*(#watch%3D|watch/)(?P\w+)", # noqa: E501 55 | ) 56 | self.add_inline( 57 | md, 58 | "vimeo", 59 | Vimeo, 60 | r"([^(]|^)http://(www.|)vimeo\.com/(?P\d+)\S*", # noqa: E501 61 | ) 62 | self.add_inline( 63 | md, "yahoo", Yahoo, r"([^(]|^)http://screen\.yahoo\.com/.+/?" 64 | ) # noqa: E501 65 | self.add_inline( 66 | md, 67 | "youtube", 68 | Youtube, 69 | r"([^(]|^)https?://www\.youtube\.com/watch\?\S*v=(?P\S[^&/]+)", # noqa: E501 70 | ) 71 | self.add_inline( 72 | md, 73 | "youtube_short", 74 | Youtube, 75 | r"([^(]|^)https?://youtu\.be/(?P\S[^?&/]+)?", 76 | ) 77 | 78 | 79 | class Dailymotion(markdown.inlinepatterns.Pattern): 80 | def handleMatch(self, m): 81 | url = "//www.dailymotion.com/embed/video/%s" % m.group("dailymotionid") 82 | width = self.ext.config["dailymotion_width"][0] 83 | height = self.ext.config["dailymotion_height"][0] 84 | return render_iframe(url, width, height) 85 | 86 | 87 | class Metacafe(markdown.inlinepatterns.Pattern): 88 | def handleMatch(self, m): 89 | url = "//www.metacafe.com/embed/%s/" % m.group("metacafeid") 90 | width = self.ext.config["metacafe_width"][0] 91 | height = self.ext.config["metacafe_height"][0] 92 | return render_iframe(url, width, height) 93 | 94 | 95 | class Veoh(markdown.inlinepatterns.Pattern): 96 | def handleMatch(self, m): 97 | url = "//www.veoh.com/videodetails2.swf?permalinkId=%s" % m.group( 98 | "veohid" 99 | ) # noqa: E501 100 | width = self.ext.config["veoh_width"][0] 101 | height = self.ext.config["veoh_height"][0] 102 | return flash_object(url, width, height) 103 | 104 | 105 | class Vimeo(markdown.inlinepatterns.Pattern): 106 | def handleMatch(self, m): 107 | url = "//player.vimeo.com/video/%s" % m.group("vimeoid") 108 | width = self.ext.config["vimeo_width"][0] 109 | height = self.ext.config["vimeo_height"][0] 110 | return render_iframe(url, width, height) 111 | 112 | 113 | class Yahoo(markdown.inlinepatterns.Pattern): 114 | def handleMatch(self, m): 115 | url = m.string + "?format=embed&player_autoplay=false" 116 | width = self.ext.config["yahoo_width"][0] 117 | height = self.ext.config["yahoo_height"][0] 118 | return render_iframe(url, width, height) 119 | 120 | 121 | class Youtube(markdown.inlinepatterns.Pattern): 122 | def handleMatch(self, m): 123 | if self.ext.config["youtube_nocookie"][0]: 124 | url = "//www.youtube-nocookie.com/embed/%s" % m.group("youtubeid") 125 | else: 126 | url = "//www.youtube.com/embed/%s" % m.group("youtubeid") 127 | width = self.ext.config["youtube_width"][0] 128 | height = self.ext.config["youtube_height"][0] 129 | return render_iframe(url, width, height) 130 | 131 | 132 | def render_iframe(url, width, height): 133 | iframe = ElementTree.Element("iframe") 134 | iframe.set("width", width) 135 | iframe.set("height", height) 136 | iframe.set("src", url) 137 | iframe.set("allowfullscreen", "true") 138 | iframe.set("frameborder", "0") 139 | return iframe 140 | 141 | 142 | def flash_object(url, width, height): 143 | obj = ElementTree.Element("object") 144 | obj.set("type", "application/x-shockwave-flash") 145 | obj.set("width", width) 146 | obj.set("height", height) 147 | obj.set("data", url) 148 | param = ElementTree.Element("param") 149 | param.set("name", "movie") 150 | param.set("value", url) 151 | obj.append(param) 152 | param = ElementTree.Element("param") 153 | param.set("name", "allowFullScreen") 154 | param.set("value", "true") 155 | obj.append(param) 156 | return obj 157 | 158 | 159 | def makeExtension(**kwargs): 160 | return VideoExtension(**kwargs) 161 | 162 | 163 | if __name__ == "__main__": 164 | import doctest 165 | 166 | doctest.testmod() 167 | -------------------------------------------------------------------------------- /martor/templates/martor/semantic/guide.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | 193 | -------------------------------------------------------------------------------- /tests/collect-static/test-collectstatic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Final Demo: ACE Icons collectstatic Fix 4 | # This script demonstrates the error condition and the fix 5 | 6 | echo "🎭 FINAL DEMONSTRATION: ACE Icons Fix" 7 | echo "==========================================" 8 | echo "This script will demonstrate:" 9 | echo "1. ✅ Current working state" 10 | echo "2. ❌ Error simulation (missing icons)" 11 | echo "3. ✅ Fix verification (restored icons)" 12 | echo 13 | 14 | # Colors 15 | GREEN='\033[0;32m' 16 | RED='\033[0;31m' 17 | YELLOW='\033[1;33m' 18 | BLUE='\033[0;34m' 19 | NC='\033[0m' 20 | 21 | print_step() { 22 | echo -e "${BLUE}[STEP]${NC} $1" 23 | } 24 | 25 | print_success() { 26 | echo -e "${GREEN}[SUCCESS]${NC} $1" 27 | } 28 | 29 | print_error() { 30 | echo -e "${RED}[ERROR]${NC} $1" 31 | } 32 | 33 | print_info() { 34 | echo -e "${YELLOW}[INFO]${NC} $1" 35 | } 36 | 37 | # Go to project root 38 | cd ../../ 39 | 40 | # Step 1: Check current state 41 | print_step "1. Checking current icon files..." 42 | icon_count=$(ls martor/static/plugins/css/main-*.png martor/static/plugins/css/main-*.svg 2>/dev/null | wc -l) 43 | echo "Found $icon_count icon files" 44 | 45 | if [ $icon_count -eq 26 ]; then 46 | print_success "All 26 required icon files are present" 47 | else 48 | print_error "Missing icon files! Expected 26, found $icon_count" 49 | fi 50 | 51 | echo 52 | 53 | # Step 2: Test current working state 54 | print_step "2. Testing current state with collectstatic..." 55 | if python3 -c " 56 | import os, sys, django 57 | from django.conf import settings 58 | from django.core.management import execute_from_command_line 59 | import shutil 60 | 61 | settings.configure( 62 | DEBUG=False, 63 | SECRET_KEY='test', 64 | INSTALLED_APPS=['django.contrib.staticfiles', 'martor'], 65 | STATIC_URL='/static/', 66 | STATIC_ROOT='/tmp/test_static_working', 67 | STATICFILES_STORAGE='whitenoise.storage.CompressedManifestStaticFilesStorage' 68 | ) 69 | django.setup() 70 | 71 | if os.path.exists('/tmp/test_static_working'): 72 | shutil.rmtree('/tmp/test_static_working') 73 | sys.argv = ['test', 'collectstatic', '--noinput'] 74 | execute_from_command_line(sys.argv) 75 | " > /dev/null 2>&1; then 76 | print_success "✅ Current state: collectstatic SUCCEEDS with all icons present" 77 | else 78 | print_error "❌ Current state: collectstatic FAILED (unexpected)" 79 | fi 80 | 81 | echo 82 | 83 | # Step 3: Simulate error by removing icons 84 | print_step "3. Simulating original error condition..." 85 | print_info "Backing up icon files to /tmp/ace-backup..." 86 | mkdir -p /tmp/ace-backup 87 | cp martor/static/plugins/css/main-*.png /tmp/ace-backup/ 2>/dev/null || true 88 | cp martor/static/plugins/css/main-*.svg /tmp/ace-backup/ 2>/dev/null || true 89 | 90 | backup_count=$(ls /tmp/ace-backup/ | wc -l) 91 | print_info "Backed up $backup_count files" 92 | 93 | print_info "Removing 5 critical icon files to simulate the original error..." 94 | rm -f martor/static/plugins/css/main-1.png 95 | rm -f martor/static/plugins/css/main-2.png 96 | rm -f martor/static/plugins/css/main-5.svg 97 | rm -f martor/static/plugins/css/main-25.svg 98 | rm -f martor/static/plugins/css/main-26.png 99 | 100 | remaining_count=$(ls martor/static/plugins/css/main-*.png martor/static/plugins/css/main-*.svg 2>/dev/null | wc -l) 101 | print_info "Now only $remaining_count icon files remain (removed 5)" 102 | 103 | echo 104 | 105 | # Step 4: Test error condition 106 | print_step "4. Testing with missing icons (should fail)..." 107 | if python3 -c " 108 | import os, sys, django 109 | from django.conf import settings 110 | from django.core.management import execute_from_command_line 111 | import shutil 112 | 113 | settings.configure( 114 | DEBUG=False, 115 | SECRET_KEY='test', 116 | INSTALLED_APPS=['django.contrib.staticfiles', 'martor'], 117 | STATIC_URL='/static/', 118 | STATIC_ROOT='/tmp/test_static_error', 119 | STATICFILES_STORAGE='whitenoise.storage.CompressedManifestStaticFilesStorage' 120 | ) 121 | django.setup() 122 | 123 | if os.path.exists('/tmp/test_static_error'): 124 | shutil.rmtree('/tmp/test_static_error') 125 | sys.argv = ['test', 'collectstatic', '--noinput'] 126 | execute_from_command_line(sys.argv) 127 | " 2>&1; then 128 | print_error "❌ Unexpected: collectstatic succeeded with missing files" 129 | else 130 | print_success "✅ Expected: collectstatic FAILED with missing icon files" 131 | print_info "This demonstrates the original MissingFileError!" 132 | fi 133 | 134 | echo 135 | 136 | # Step 5: Restore files 137 | print_step "5. Restoring icon files..." 138 | cp /tmp/ace-backup/* martor/static/plugins/css/ 139 | rm -rf /tmp/ace-backup 140 | 141 | restored_count=$(ls martor/static/plugins/css/main-*.png martor/static/plugins/css/main-*.svg 2>/dev/null | wc -l) 142 | print_success "Restored all files. Total: $restored_count" 143 | 144 | echo 145 | 146 | # Step 6: Test success condition 147 | print_step "6. Testing with all icons restored..." 148 | if python3 -c " 149 | import os, sys, django 150 | from django.conf import settings 151 | from django.core.management import execute_from_command_line 152 | import shutil 153 | 154 | settings.configure( 155 | DEBUG=False, 156 | SECRET_KEY='test', 157 | INSTALLED_APPS=['django.contrib.staticfiles', 'martor'], 158 | STATIC_URL='/static/', 159 | STATIC_ROOT='/tmp/test_static_success', 160 | STATICFILES_STORAGE='whitenoise.storage.CompressedManifestStaticFilesStorage' 161 | ) 162 | django.setup() 163 | 164 | if os.path.exists('/tmp/test_static_success'): 165 | shutil.rmtree('/tmp/test_static_success') 166 | sys.argv = ['test', 'collectstatic', '--noinput'] 167 | execute_from_command_line(sys.argv) 168 | " > /dev/null 2>&1; then 169 | print_success "✅ SUCCESS: collectstatic WORKS with all icon files present" 170 | else 171 | print_error "❌ Unexpected: collectstatic still failing" 172 | fi 173 | 174 | # Cleanup 175 | rm -rf /tmp/test_static_working /tmp/test_static_error /tmp/test_static_success 2>/dev/null 176 | 177 | echo 178 | echo "==========================================" 179 | echo -e "${GREEN}🎉 DEMONSTRATION COMPLETE!${NC}" 180 | echo 181 | echo "SUMMARY:" 182 | echo "1. ✅ With all 26 icon files → collectstatic SUCCEEDS" 183 | echo "2. ❌ Missing icon files → collectstatic FAILS (MissingFileError)" 184 | echo "3. ✅ Restored icon files → collectstatic SUCCEEDS again" 185 | echo 186 | echo "THE FIX:" 187 | echo "- Downloaded 26 missing icon files from ACE repository" 188 | echo "- Files: main-1.png through main-26.png, main-5.svg through main-25.svg" 189 | echo "- Location: martor/static/plugins/css/" 190 | echo 191 | print_success "The collectstatic issue has been permanently resolved!" 192 | echo "==========================================" 193 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | This guide walks you through installing and setting up Martor in your Django project. 5 | 6 | Requirements 7 | ------------ 8 | 9 | Martor requires the following: 10 | 11 | * **Python**: 3.9+ 12 | * **Django**: 3.2+ 13 | * **Markdown**: 3.0+ 14 | * **Additional Dependencies**: 15 | 16 | * ``requests`` >= 2.12.4 17 | * ``bleach`` (for HTML sanitization) 18 | * ``urllib3`` 19 | * ``zipp`` 20 | * ``tzdata`` 21 | 22 | Installing Martor 23 | ------------------ 24 | 25 | 1. Install via pip 26 | ~~~~~~~~~~~~~~~~~~ 27 | 28 | The easiest way to install Martor is through pip: 29 | 30 | .. code-block:: bash 31 | 32 | pip install martor 33 | 34 | 2. Install from source 35 | ~~~~~~~~~~~~~~~~~~~~~~ 36 | 37 | For the latest development version: 38 | 39 | .. code-block:: bash 40 | 41 | git clone https://github.com/agusmakmun/django-markdown-editor.git 42 | cd django-markdown-editor 43 | pip install -e . 44 | 45 | Django Configuration 46 | --------------------- 47 | 48 | 1. Add to INSTALLED_APPS 49 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 50 | 51 | Add ``'martor'`` to your ``INSTALLED_APPS`` in settings.py: 52 | 53 | .. code-block:: python 54 | 55 | # settings.py 56 | INSTALLED_APPS = [ 57 | 'django.contrib.admin', 58 | 'django.contrib.auth', 59 | 'django.contrib.contenttypes', 60 | 'django.contrib.sessions', 61 | 'django.contrib.messages', 62 | 'django.contrib.staticfiles', 63 | 64 | # Your apps 65 | 'your_app', 66 | 67 | # Third party apps 68 | 'martor', # Add this line 69 | ] 70 | 71 | .. note:: 72 | Martor doesn't require database migrations, so you don't need to run ``makemigrations`` or ``migrate`` after adding it. 73 | 74 | 2. Configure URLs 75 | ~~~~~~~~~~~~~~~~~ 76 | 77 | Add Martor URLs to your main ``urls.py``: 78 | 79 | .. code-block:: python 80 | 81 | # urls.py 82 | from django.contrib import admin 83 | from django.urls import path, include 84 | 85 | urlpatterns = [ 86 | path('admin/', admin.site.urls), 87 | path('martor/', include('martor.urls')), # Add this line 88 | # ... your other URL patterns 89 | ] 90 | 91 | The Martor URLs provide essential endpoints for: 92 | 93 | * ``/martor/markdownify/`` - Live preview conversion 94 | * ``/martor/uploader/`` - Image upload handling 95 | * ``/martor/search-user/`` - User mention search 96 | 97 | 3. Collect Static Files 98 | ~~~~~~~~~~~~~~~~~~~~~~~ 99 | 100 | Martor includes CSS and JavaScript files that need to be collected: 101 | 102 | .. code-block:: bash 103 | 104 | python manage.py collectstatic 105 | 106 | This will copy Martor's static files to your ``STATIC_ROOT`` directory. 107 | 108 | Essential Settings 109 | ------------------ 110 | 111 | While Martor works with default settings, you'll want to configure a few key options: 112 | 113 | .. code-block:: python 114 | 115 | # settings.py 116 | 117 | # Choose your preferred theme: "bootstrap" or "semantic" 118 | MARTOR_THEME = 'bootstrap' # Default 119 | 120 | # CSRF token configuration (required for AJAX uploads) 121 | CSRF_COOKIE_HTTPONLY = False 122 | 123 | # Optional: Configure imgur for image uploads 124 | MARTOR_IMGUR_CLIENT_ID = 'your-imgur-client-id' 125 | MARTOR_IMGUR_API_KEY = 'your-imgur-api-key' 126 | 127 | .. warning:: 128 | Setting ``CSRF_COOKIE_HTTPONLY = False`` is required for Martor's AJAX functionality to work properly. This allows the CSRF token to be accessible via JavaScript for secure AJAX requests. 129 | 130 | Imgur Configuration (Optional) 131 | ------------------------------- 132 | 133 | For image uploads to work with imgur.com: 134 | 135 | 1. **Register your application** at https://api.imgur.com/oauth2/addclient 136 | 2. **Get your credentials** from the imgur developer portal 137 | 3. **Add them to your settings**: 138 | 139 | .. code-block:: python 140 | 141 | # settings.py 142 | MARTOR_IMGUR_CLIENT_ID = 'your-client-id-here' 143 | MARTOR_IMGUR_API_KEY = 'your-api-key-here' 144 | 145 | Alternatively, you can set up a :doc:`custom uploader ` to handle image uploads to your own storage backend. 146 | 147 | Verification 148 | ------------ 149 | 150 | To verify your installation is working: 151 | 152 | 1. **Start your development server**: 153 | 154 | .. code-block:: bash 155 | 156 | python manage.py runserver 157 | 158 | 2. **Create a simple test view** (optional): 159 | 160 | .. code-block:: python 161 | 162 | # views.py 163 | from django.shortcuts import render 164 | from django import forms 165 | from martor.fields import MartorFormField 166 | 167 | class TestForm(forms.Form): 168 | content = MartorFormField() 169 | 170 | def test_martor(request): 171 | form = TestForm() 172 | return render(request, 'test_martor.html', {'form': form}) 173 | 174 | 3. **Create a test template**: 175 | 176 | .. code-block:: html 177 | 178 | 179 | 180 | 181 | 182 | Martor Test 183 | {% load static %} 184 | 185 | 186 | 187 | 188 |
189 |

Martor Test

190 | 191 | {% csrf_token %} 192 | {{ form.as_p }} 193 | 194 | 195 |
196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | Troubleshooting Installation 208 | ---------------------------- 209 | 210 | **Static files not loading?** 211 | Make sure you've run ``collectstatic`` and your ``STATIC_URL`` and ``STATIC_ROOT`` are properly configured. 212 | 213 | **CSRF errors during AJAX requests?** 214 | Ensure ``CSRF_COOKIE_HTTPONLY = False`` is set in your settings. 215 | 216 | **Import errors?** 217 | Verify that Martor is properly installed: ``pip show martor`` 218 | 219 | **JavaScript errors?** 220 | Check that all required static files are properly loaded. Use browser developer tools to identify missing files. 221 | 222 | Next Steps 223 | ---------- 224 | 225 | Now that Martor is installed, continue with: 226 | 227 | * :doc:`quickstart` - Basic usage examples 228 | * :doc:`settings` - Complete configuration reference 229 | * :doc:`usage/models` - Using Martor in your models 230 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | import os 7 | import sys 8 | 9 | # Add the project root to the Python path 10 | sys.path.insert(0, os.path.abspath("..")) 11 | 12 | # Import Django settings 13 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "martor_demo.martor_demo.settings") 14 | 15 | try: 16 | import django 17 | 18 | django.setup() 19 | except ImportError: 20 | pass 21 | 22 | # -- Project information ----------------------------------------------------- 23 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 24 | 25 | project = "Martor" 26 | copyright = "2025, Agus Makmun (Summon Agus)" 27 | author = "Agus Makmun (Summon Agus)" 28 | 29 | # The version info for the project you're documenting, acts as replacement for 30 | # |version| and |release|, also used in various other places throughout the 31 | # built documents. 32 | # 33 | # Import version from package 34 | try: 35 | from martor import __version__ 36 | 37 | version = __version__ 38 | release = __version__ 39 | except ImportError: 40 | version = "1.7.16" 41 | release = "1.7.16" 42 | 43 | # -- General configuration --------------------------------------------------- 44 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 45 | 46 | extensions = [ 47 | "sphinx.ext.autodoc", 48 | "sphinx.ext.autosummary", 49 | "sphinx.ext.doctest", 50 | "sphinx.ext.intersphinx", 51 | "sphinx.ext.todo", 52 | "sphinx.ext.coverage", 53 | "sphinx.ext.mathjax", 54 | "sphinx.ext.ifconfig", 55 | "sphinx.ext.viewcode", 56 | "sphinx.ext.githubpages", 57 | "sphinx.ext.napoleon", 58 | ] 59 | 60 | # Add any paths that contain templates here, relative to this directory. 61 | templates_path = ["_templates"] 62 | 63 | # List of patterns, relative to source directory, that match files and 64 | # directories to ignore when looking for source files. 65 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 66 | 67 | # The language for content autogenerated by Sphinx 68 | language = "en" 69 | 70 | # -- Options for HTML output ------------------------------------------------- 71 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 72 | 73 | html_theme = "sphinx_rtd_theme" 74 | html_static_path = ["_static"] 75 | 76 | # Theme options are theme-specific and customize the look and feel of a theme 77 | # further. For a list of options available for each theme, see the 78 | # documentation. 79 | html_theme_options = { 80 | "canonical_url": "", 81 | "analytics_id": "", 82 | "logo_only": False, 83 | "display_version": True, 84 | "prev_next_buttons_location": "bottom", 85 | "style_external_links": False, 86 | "vcs_pageview_mode": "", 87 | # 'style_nav_header_background': '#2980B9', 88 | # Toc options 89 | "collapse_navigation": True, 90 | "sticky_navigation": True, 91 | "navigation_depth": 4, 92 | "includehidden": True, 93 | "titles_only": False, 94 | } 95 | 96 | # Custom sidebar templates, must be a dictionary that maps document names 97 | # to template names. 98 | html_sidebars = { 99 | "**": [ 100 | "relations.html", # needs 'show_related': True theme option to display 101 | "searchbox.html", 102 | ] 103 | } 104 | 105 | # The name of an image file (relative to this directory) to place at the top 106 | # of the sidebar. 107 | # html_logo = None 108 | 109 | # The name of an image file (within the static path) to use as favicon of the 110 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 111 | # pixels large. 112 | # html_favicon = None 113 | 114 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 115 | # using the given strftime format. 116 | html_last_updated_fmt = "%b %d, %Y" 117 | 118 | # If true, SmartyPants will be used to convert quotes and dashes to 119 | # typographically correct entities. 120 | html_use_smartypants = True 121 | 122 | # Additional templates that should be rendered to pages, maps page names to 123 | # template names. 124 | # html_additional_pages = {} 125 | 126 | # If false, no module index is generated. 127 | html_domain_indices = True 128 | 129 | # If false, no index is generated. 130 | html_use_index = True 131 | 132 | # If true, the index is split into individual pages for each letter. 133 | html_split_index = False 134 | 135 | # If true, links to the reST sources are added to the pages. 136 | html_show_sourcelink = True 137 | 138 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 139 | html_show_sphinx = True 140 | 141 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 142 | html_show_copyright = True 143 | 144 | # If true, an OpenSearch description file will be output, and all pages will 145 | # contain a tag referring to it. The value of this option must be the 146 | # base URL from which the finished HTML is served. 147 | # html_use_opensearch = '' 148 | 149 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 150 | # html_file_suffix = None 151 | 152 | # Output file base name for HTML help builder. 153 | htmlhelp_basename = "Martordoc" 154 | 155 | # -- Options for autodoc extension ------------------------------------------ 156 | 157 | # This value controls the docstrings inheritance. 158 | autodoc_inherit_docstrings = True 159 | 160 | # This value selects what content will be inserted into the main body of an autoclass directive. 161 | autodoc_default_options = { 162 | "members": True, 163 | "member-order": "bysource", 164 | "special-members": "__init__", 165 | "undoc-members": True, 166 | "exclude-members": "__weakref__", 167 | } 168 | 169 | # -- Options for intersphinx extension -------------------------------------- 170 | 171 | # Example configuration for intersphinx: refer to the Python standard library. 172 | intersphinx_mapping = { 173 | "python": ("https://docs.python.org/3", None), 174 | "django": ("https://docs.djangoproject.com/en/stable/", "https://docs.djangoproject.com/en/stable/_objects/"), 175 | "markdown": ("https://python-markdown.github.io/", None), 176 | } 177 | 178 | # -- Options for todo extension --------------------------------------------- 179 | 180 | # If true, `todo` and `todoList` produce output, else they produce nothing. 181 | todo_include_todos = True 182 | 183 | # -- Options for Napoleon extension ---------------------------------------- 184 | 185 | napoleon_google_docstring = True 186 | napoleon_numpy_docstring = True 187 | napoleon_include_init_with_doc = False 188 | napoleon_include_private_with_doc = False 189 | napoleon_include_special_with_doc = True 190 | napoleon_use_admonition_for_examples = False 191 | napoleon_use_admonition_for_notes = False 192 | napoleon_use_admonition_for_references = False 193 | napoleon_use_ivar = False 194 | napoleon_use_param = True 195 | napoleon_use_rtype = True 196 | napoleon_type_aliases = None 197 | napoleon_preprocess_types = False 198 | napoleon_attr_annotations = True 199 | -------------------------------------------------------------------------------- /martor/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | # Choices are: "semantic", "bootstrap" 4 | MARTOR_THEME = getattr(settings, "MARTOR_THEME", "bootstrap") 5 | 6 | # Global martor settings 7 | # Input: string boolean, `true/false` 8 | MARTOR_ENABLE_CONFIGS = getattr( 9 | settings, 10 | "MARTOR_ENABLE_CONFIGS", 11 | { 12 | "emoji": "true", # enable/disable emoji icons. 13 | "imgur": "true", # enable/disable imgur/custom uploader. 14 | "mention": "false", # enable/disable mention 15 | "jquery": "true", # include/revoke jquery (require for admin django) 16 | "living": "false", # enable/disable live updates in preview 17 | "spellcheck": "false", # enable/disable spellcheck in form textareas 18 | "hljs": "true", # enable/disable hljs highlighting in preview 19 | }, 20 | ) 21 | 22 | # To show the toolbar buttons 23 | MARTOR_TOOLBAR_BUTTONS = getattr( 24 | settings, 25 | "MARTOR_TOOLBAR_BUTTONS", 26 | [ 27 | "bold", 28 | "italic", 29 | "horizontal", 30 | "heading", 31 | "pre-code", 32 | "blockquote", 33 | "unordered-list", 34 | "ordered-list", 35 | "link", 36 | "image-link", 37 | "image-upload", 38 | "emoji", 39 | "direct-mention", 40 | "toggle-maximize", 41 | "help", 42 | ], 43 | ) 44 | 45 | # To setup the martor editor with title label or not (default is False) 46 | MARTOR_ENABLE_LABEL = getattr(settings, "MARTOR_ENABLE_LABEL", False) 47 | 48 | # Imgur API Keys 49 | MARTOR_IMGUR_CLIENT_ID = getattr(settings, "MARTOR_IMGUR_CLIENT_ID", "") 50 | MARTOR_IMGUR_API_KEY = getattr(settings, "MARTOR_IMGUR_API_KEY", "") 51 | 52 | # Markdownify 53 | MARTOR_MARKDOWNIFY_FUNCTION = getattr( 54 | settings, "MARTOR_MARKDOWNIFY_FUNCTION", "martor.utils.markdownify" 55 | ) 56 | MARTOR_MARKDOWNIFY_URL = getattr( 57 | settings, "MARTOR_MARKDOWNIFY_URL", "/martor/markdownify/" 58 | ) 59 | 60 | # Time to delay the markdownify ajax request, in millisecond. 61 | MARTOR_MARKDOWNIFY_TIMEOUT = getattr(settings, "MARTOR_MARKDOWNIFY_TIMEOUT", 1000) 62 | 63 | # Markdown extensions 64 | MARTOR_MARKDOWN_EXTENSIONS = getattr( 65 | settings, 66 | "MARTOR_MARKDOWN_EXTENSIONS", 67 | [ 68 | "markdown.extensions.extra", 69 | "markdown.extensions.nl2br", 70 | "markdown.extensions.smarty", 71 | "markdown.extensions.fenced_code", 72 | "markdown.extensions.sane_lists", 73 | # Custom markdown extensions. 74 | "martor.extensions.urlize", 75 | "martor.extensions.del_ins", # ~~strikethrough~~ and ++underscores++ 76 | "martor.extensions.mention", # to parse markdown mention 77 | "martor.extensions.emoji", # to parse markdown emoji 78 | "martor.extensions.mdx_video", # to parse embed/iframe video 79 | "martor.extensions.escape_html", # to handle the XSS vulnerabilities 80 | "martor.extensions.mdx_add_id", # to parse id like {#this_is_id} 81 | ], 82 | ) 83 | 84 | # Markdown Extensions Configs 85 | MARTOR_MARKDOWN_EXTENSION_CONFIGS = getattr( 86 | settings, "MARTOR_MARKDOWN_EXTENSION_CONFIGS", {} 87 | ) 88 | 89 | # Markdown urls 90 | MARTOR_UPLOAD_URL = ( 91 | # Allows to disable this endpoint 92 | settings.MARTOR_UPLOAD_URL 93 | if hasattr(settings, "MARTOR_UPLOAD_URL") 94 | else "/martor/uploader/" 95 | ) 96 | 97 | MARTOR_SEARCH_USERS_URL = ( 98 | # Allows to disable this endpoint 99 | settings.MARTOR_SEARCH_USERS_URL 100 | if hasattr(settings, "MARTOR_SEARCH_USERS_URL") 101 | else "/martor/search-user/" 102 | ) 103 | 104 | # Markdown Extensions 105 | MARTOR_MARKDOWN_BASE_EMOJI_URL = ( 106 | # Allows to disable this endpoint 107 | settings.MARTOR_MARKDOWN_BASE_EMOJI_URL 108 | if hasattr(settings, "MARTOR_MARKDOWN_BASE_EMOJI_URL") 109 | else "https://github.githubassets.com/images/icons/emoji/" 110 | ) 111 | 112 | MARTOR_MARKDOWN_BASE_MENTION_URL = getattr( 113 | settings, 114 | "MARTOR_MARKDOWN_BASE_MENTION_URL", 115 | "", 116 | ) 117 | 118 | # If you need to use your own themed "bootstrap" or "semantic ui" dependency 119 | # replace the values with the file in your static files dir 120 | MARTOR_ALTERNATIVE_JS_FILE_THEME = getattr( 121 | settings, "MARTOR_ALTERNATIVE_JS_FILE_THEME", None 122 | ) 123 | MARTOR_ALTERNATIVE_CSS_FILE_THEME = getattr( 124 | settings, "MARTOR_ALTERNATIVE_CSS_FILE_THEME", None 125 | ) 126 | MARTOR_ALTERNATIVE_JQUERY_JS_FILE = getattr( 127 | settings, "MARTOR_ALTERNATIVE_JQUERY_JS_FILE", None 128 | ) 129 | 130 | # URL schemes that are allowed within links 131 | ALLOWED_URL_SCHEMES = getattr( 132 | settings, 133 | "ALLOWED_URL_SCHEMES", 134 | [ 135 | "file", 136 | "ftp", 137 | "ftps", 138 | "http", 139 | "https", 140 | "irc", 141 | "mailto", 142 | "sftp", 143 | "ssh", 144 | "tel", 145 | "telnet", 146 | "tftp", 147 | "vnc", 148 | "xmpp", 149 | ], 150 | ) 151 | 152 | # https://gist.github.com/mrmrs/7650266 153 | ALLOWED_HTML_TAGS = getattr( 154 | settings, 155 | "ALLOWED_HTML_TAGS", 156 | [ 157 | "a", 158 | "abbr", 159 | "b", 160 | "blockquote", 161 | "br", 162 | "cite", 163 | "code", 164 | "command", 165 | "dd", 166 | "del", 167 | "dl", 168 | "dt", 169 | "em", 170 | "fieldset", 171 | "h1", 172 | "h2", 173 | "h3", 174 | "h4", 175 | "h5", 176 | "h6", 177 | "hr", 178 | "i", 179 | "iframe", 180 | "img", 181 | "input", 182 | "ins", 183 | "kbd", 184 | "label", 185 | "legend", 186 | "li", 187 | "ol", 188 | "optgroup", 189 | "option", 190 | "p", 191 | "pre", 192 | "small", 193 | "span", 194 | "strong", 195 | "sub", 196 | "sup", 197 | "table", 198 | "tbody", 199 | "td", 200 | "tfoot", 201 | "th", 202 | "thead", 203 | "tr", 204 | "u", 205 | "ul", 206 | ], 207 | ) 208 | 209 | # https://github.com/decal/werdlists/blob/master/html-words/html-attributes-list.txt 210 | ALLOWED_HTML_ATTRIBUTES = getattr( 211 | settings, 212 | "ALLOWED_HTML_ATTRIBUTES", 213 | [ 214 | "alt", 215 | "class", 216 | "color", 217 | "colspan", 218 | # "data", 219 | "datetime", 220 | "height", 221 | "href", 222 | "id", 223 | "name", 224 | "reversed", 225 | "rowspan", 226 | "scope", 227 | "src", 228 | "style", 229 | "title", 230 | "type", 231 | "width", 232 | ], 233 | ) 234 | # Disable admin style when using custom admin interface e.g django-grappelli 235 | MARTOR_ENABLE_ADMIN_CSS = getattr(settings, "MARTOR_ENABLE_ADMIN_CSS", True) 236 | 237 | MARTOR_CSRF_COOKIE_NAME = ( 238 | settings.MARTOR_CSRF_COOKIE_NAME 239 | if hasattr(settings, "MARTOR_CSRF_COOKIE_NAME") 240 | else "csrftoken" 241 | ) -------------------------------------------------------------------------------- /martor/locale/id/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 , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2025-09-03 23:52+0700\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: templates/martor/bootstrap/editor.html:11 21 | #: templates/martor/semantic/editor.html:5 22 | msgid "Editor" 23 | msgstr "" 24 | 25 | #: templates/martor/bootstrap/editor.html:18 26 | #: templates/martor/semantic/editor.html:6 27 | msgid "Preview" 28 | msgstr "Pratinjau" 29 | 30 | #: templates/martor/bootstrap/editor.html:31 31 | #: templates/martor/semantic/editor.html:11 32 | msgid "Uploading... please wait..." 33 | msgstr "Sedang mengupload... silahkan tunggu..." 34 | 35 | #: templates/martor/bootstrap/editor.html:38 36 | #: templates/martor/semantic/editor.html:18 37 | msgid "Nothing to preview" 38 | msgstr "Tidak ada yang bisa dilihat" 39 | 40 | #: templates/martor/bootstrap/emoji.html:13 41 | #: templates/martor/semantic/emoji.html:4 42 | msgid "Select Emoji to Insert" 43 | msgstr "Pilih Emoji untuk menyisipkan" 44 | 45 | #: templates/martor/bootstrap/emoji.html:22 46 | #: templates/martor/semantic/emoji.html:8 47 | msgid "Preparing emojis..." 48 | msgstr "Sedang menyiapkan emoji..." 49 | 50 | #: templates/martor/bootstrap/guide.html:14 51 | #: templates/martor/semantic/guide.html:4 52 | msgid "Markdown Guide" 53 | msgstr "Bantuan Penggunaan Markdown" 54 | 55 | #: templates/martor/bootstrap/guide.html:20 56 | #, fuzzy, python-format 57 | #| msgid "Copyright © Martor" 58 | msgid "" 59 | "Copyright © Martor" 61 | msgstr "Hak cipta © Martor" 62 | 63 | #: templates/martor/bootstrap/guide.html:24 64 | #, fuzzy, python-format 65 | #| msgid "" 66 | #| "This editor is powered by Markdown. For full documentation, click here." 68 | msgid "" 69 | "This editor is powered by Markdown. For full\n" 70 | " documentation, click " 71 | "here." 72 | msgstr "" 73 | "Editor ini menggunakan Markdown. Untuk dokumentasi lengkapnya klik disini." 75 | 76 | #: templates/martor/bootstrap/guide.html:30 77 | #: templates/martor/bootstrap/toolbar.html:53 78 | #: templates/martor/semantic/guide.html:17 79 | #: templates/martor/semantic/toolbar.html:38 80 | msgid "Code" 81 | msgstr "Kode" 82 | 83 | #: templates/martor/bootstrap/guide.html:31 84 | #: templates/martor/semantic/guide.html:18 85 | msgid "Or" 86 | msgstr "Atau" 87 | 88 | #: templates/martor/bootstrap/guide.html:34 89 | #: templates/martor/semantic/guide.html:21 90 | msgid "... to Get" 91 | msgstr "... untuk Mendapatkan" 92 | 93 | #: templates/martor/bootstrap/toolbar.html:3 94 | msgid "Martor Toolbar Buttons" 95 | msgstr "" 96 | 97 | #: templates/martor/bootstrap/toolbar.html:6 98 | #: templates/martor/semantic/toolbar.html:5 99 | msgid "Bold" 100 | msgstr "Tebal" 101 | 102 | #: templates/martor/bootstrap/toolbar.html:14 103 | #: templates/martor/semantic/toolbar.html:11 104 | msgid "Italic" 105 | msgstr "Miring" 106 | 107 | #: templates/martor/bootstrap/toolbar.html:22 108 | #: templates/martor/semantic/toolbar.html:17 109 | msgid "Horizontal Line" 110 | msgstr "Garis Horisontal" 111 | 112 | #: templates/martor/bootstrap/toolbar.html:31 113 | #: templates/martor/bootstrap/toolbar.html:37 114 | #: templates/martor/bootstrap/toolbar.html:38 115 | #: templates/martor/bootstrap/toolbar.html:39 116 | #: templates/martor/semantic/toolbar.html:23 117 | #: templates/martor/semantic/toolbar.html:26 118 | #: templates/martor/semantic/toolbar.html:27 119 | #: templates/martor/semantic/toolbar.html:28 120 | msgid "Heading" 121 | msgstr "" 122 | 123 | #: templates/martor/bootstrap/toolbar.html:46 124 | #: templates/martor/semantic/toolbar.html:34 125 | msgid "Pre or Code" 126 | msgstr "Pre atau Kode" 127 | 128 | #: templates/martor/bootstrap/toolbar.html:52 129 | #: templates/martor/semantic/toolbar.html:37 130 | msgid "Pre" 131 | msgstr "" 132 | 133 | #: templates/martor/bootstrap/toolbar.html:59 134 | #: templates/martor/semantic/toolbar.html:44 135 | msgid "Quote" 136 | msgstr "Kutipan" 137 | 138 | #: templates/martor/bootstrap/toolbar.html:68 139 | #: templates/martor/semantic/toolbar.html:50 140 | msgid "Unordered List" 141 | msgstr "Daftar tak berurut" 142 | 143 | #: templates/martor/bootstrap/toolbar.html:76 144 | #: templates/martor/semantic/toolbar.html:56 145 | msgid "Ordered List" 146 | msgstr "Daftar berurut" 147 | 148 | #: templates/martor/bootstrap/toolbar.html:85 149 | #: templates/martor/semantic/toolbar.html:62 150 | msgid "URL/Link" 151 | msgstr "" 152 | 153 | #: templates/martor/bootstrap/toolbar.html:94 154 | #: templates/martor/semantic/toolbar.html:68 155 | msgid "Insert Image Link" 156 | msgstr "Sisipkan Link Gambar" 157 | 158 | #: templates/martor/bootstrap/toolbar.html:102 159 | #: templates/martor/bootstrap/toolbar.html:107 160 | #: templates/martor/semantic/toolbar.html:74 161 | #: templates/martor/semantic/toolbar.html:76 162 | msgid "Upload an Image" 163 | msgstr "Unggah Gambar" 164 | 165 | #: templates/martor/bootstrap/toolbar.html:112 166 | #: templates/martor/semantic/toolbar.html:81 167 | msgid "Insert Emoji" 168 | msgstr "Sisipkan Emoji" 169 | 170 | #: templates/martor/bootstrap/toolbar.html:120 171 | #: templates/martor/semantic/toolbar.html:87 172 | msgid "Direct Mention User" 173 | msgstr "Sebutkan Pengguna secara Langsung" 174 | 175 | #: templates/martor/bootstrap/toolbar.html:128 176 | #: templates/martor/semantic/toolbar.html:93 177 | msgid "Full Screen" 178 | msgstr "Layar Penuh" 179 | 180 | #: templates/martor/bootstrap/toolbar.html:139 181 | #: templates/martor/semantic/toolbar.html:99 182 | msgid "Markdown Guide (Help)" 183 | msgstr "Bantuan Penggunaan Markdown" 184 | 185 | #: templates/martor/semantic/guide.html:7 186 | #, fuzzy, python-format 187 | #| msgid "Copyright © Martor" 188 | msgid "" 189 | "Copyright © Martor" 191 | msgstr "Hak cipta © Martor" 192 | 193 | #: templates/martor/semantic/guide.html:11 194 | #, fuzzy, python-format 195 | #| msgid "" 196 | #| "This editor is powered by Markdown. For full documentation, click here." 198 | msgid "" 199 | "This editor is powered by Markdown. For full\n" 200 | " documentation, click here." 202 | msgstr "" 203 | "Editor ini menggunakan Markdown. Untuk dokumentasi lengkapnya klik disini." 205 | 206 | #: views.py:19 views.py:37 views.py:38 207 | msgid "Invalid request!" 208 | msgstr "Permintaan tidak valid" 209 | 210 | #: views.py:74 211 | #, python-format 212 | msgid "No users registered as `%(username)s` or user is unactived." 213 | msgstr "" 214 | "Tidak ada pengguna terdaftar sebagai `%(username)s` atau pengguna telah " 215 | "nonaktif." 216 | 217 | #: views.py:79 218 | msgid "Validation Failed for field `username`" 219 | msgstr "Validasi Gagal untuk field `username`" 220 | -------------------------------------------------------------------------------- /martor/locale/tr/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 , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2025-09-03 23:54+0700\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Ozcan Yarimdunya \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: templates/martor/bootstrap/editor.html:11 22 | #: templates/martor/semantic/editor.html:5 23 | msgid "Editor" 24 | msgstr "Editör" 25 | 26 | #: templates/martor/bootstrap/editor.html:18 27 | #: templates/martor/semantic/editor.html:6 28 | msgid "Preview" 29 | msgstr "Ön izleme" 30 | 31 | #: templates/martor/bootstrap/editor.html:31 32 | #: templates/martor/semantic/editor.html:11 33 | msgid "Uploading... please wait..." 34 | msgstr "Yükleniyor ... Lütfen Bekleyin..." 35 | 36 | #: templates/martor/bootstrap/editor.html:38 37 | #: templates/martor/semantic/editor.html:18 38 | msgid "Nothing to preview" 39 | msgstr "Önizlenecek bir şey yok" 40 | 41 | #: templates/martor/bootstrap/emoji.html:13 42 | #: templates/martor/semantic/emoji.html:4 43 | msgid "Select Emoji to Insert" 44 | msgstr "Eklenecek Emojiyi Seçin" 45 | 46 | #: templates/martor/bootstrap/emoji.html:22 47 | #: templates/martor/semantic/emoji.html:8 48 | msgid "Preparing emojis..." 49 | msgstr "Emojiler hazırlanıyor ..." 50 | 51 | #: templates/martor/bootstrap/guide.html:14 52 | #: templates/martor/semantic/guide.html:4 53 | msgid "Markdown Guide" 54 | msgstr "Markdown Kılavuzu" 55 | 56 | #: templates/martor/bootstrap/guide.html:20 57 | #, fuzzy, python-format 58 | #| msgid "Copyright © Martor" 59 | msgid "" 60 | "Copyright © Martor" 62 | msgstr "Telif hakkı © Martor" 63 | 64 | #: templates/martor/bootstrap/guide.html:24 65 | #, fuzzy, python-format 66 | #| msgid "" 67 | #| "This editor is powered by Markdown. For full documentation, click here." 69 | msgid "" 70 | "This editor is powered by Markdown. For full\n" 71 | " documentation, click " 72 | "here." 73 | msgstr "" 74 | "Bu editör Markdown tarafından desteklenmektedir. Bütün dokümantasyon için, " 75 | "buraya tıkla." 76 | 77 | #: templates/martor/bootstrap/guide.html:30 78 | #: templates/martor/bootstrap/toolbar.html:53 79 | #: templates/martor/semantic/guide.html:17 80 | #: templates/martor/semantic/toolbar.html:38 81 | msgid "Code" 82 | msgstr "Kod" 83 | 84 | #: templates/martor/bootstrap/guide.html:31 85 | #: templates/martor/semantic/guide.html:18 86 | msgid "Or" 87 | msgstr "Veya" 88 | 89 | #: templates/martor/bootstrap/guide.html:34 90 | #: templates/martor/semantic/guide.html:21 91 | msgid "... to Get" 92 | msgstr "... Almak için" 93 | 94 | #: templates/martor/bootstrap/toolbar.html:3 95 | msgid "Martor Toolbar Buttons" 96 | msgstr "" 97 | 98 | #: templates/martor/bootstrap/toolbar.html:6 99 | #: templates/martor/semantic/toolbar.html:5 100 | msgid "Bold" 101 | msgstr "Kalın" 102 | 103 | #: templates/martor/bootstrap/toolbar.html:14 104 | #: templates/martor/semantic/toolbar.html:11 105 | msgid "Italic" 106 | msgstr "İtalik" 107 | 108 | #: templates/martor/bootstrap/toolbar.html:22 109 | #: templates/martor/semantic/toolbar.html:17 110 | msgid "Horizontal Line" 111 | msgstr "Yatay çizgi" 112 | 113 | #: templates/martor/bootstrap/toolbar.html:31 114 | #: templates/martor/bootstrap/toolbar.html:37 115 | #: templates/martor/bootstrap/toolbar.html:38 116 | #: templates/martor/bootstrap/toolbar.html:39 117 | #: templates/martor/semantic/toolbar.html:23 118 | #: templates/martor/semantic/toolbar.html:26 119 | #: templates/martor/semantic/toolbar.html:27 120 | #: templates/martor/semantic/toolbar.html:28 121 | msgid "Heading" 122 | msgstr "Başlık" 123 | 124 | #: templates/martor/bootstrap/toolbar.html:46 125 | #: templates/martor/semantic/toolbar.html:34 126 | msgid "Pre or Code" 127 | msgstr "Ön veya Kod" 128 | 129 | #: templates/martor/bootstrap/toolbar.html:52 130 | #: templates/martor/semantic/toolbar.html:37 131 | msgid "Pre" 132 | msgstr "Ön" 133 | 134 | #: templates/martor/bootstrap/toolbar.html:59 135 | #: templates/martor/semantic/toolbar.html:44 136 | msgid "Quote" 137 | msgstr "Alıntı" 138 | 139 | #: templates/martor/bootstrap/toolbar.html:68 140 | #: templates/martor/semantic/toolbar.html:50 141 | msgid "Unordered List" 142 | msgstr "Sırasız liste" 143 | 144 | #: templates/martor/bootstrap/toolbar.html:76 145 | #: templates/martor/semantic/toolbar.html:56 146 | msgid "Ordered List" 147 | msgstr "Sıralı Liste" 148 | 149 | #: templates/martor/bootstrap/toolbar.html:85 150 | #: templates/martor/semantic/toolbar.html:62 151 | msgid "URL/Link" 152 | msgstr "URL/Link" 153 | 154 | #: templates/martor/bootstrap/toolbar.html:94 155 | #: templates/martor/semantic/toolbar.html:68 156 | msgid "Insert Image Link" 157 | msgstr "Resim Bağlantısı Ekle" 158 | 159 | #: templates/martor/bootstrap/toolbar.html:102 160 | #: templates/martor/bootstrap/toolbar.html:107 161 | #: templates/martor/semantic/toolbar.html:74 162 | #: templates/martor/semantic/toolbar.html:76 163 | msgid "Upload an Image" 164 | msgstr "Bir Resim Yükle" 165 | 166 | #: templates/martor/bootstrap/toolbar.html:112 167 | #: templates/martor/semantic/toolbar.html:81 168 | msgid "Insert Emoji" 169 | msgstr "Emoji Ekle" 170 | 171 | #: templates/martor/bootstrap/toolbar.html:120 172 | #: templates/martor/semantic/toolbar.html:87 173 | msgid "Direct Mention User" 174 | msgstr "Bir Kullanıcıdan Doğrudan Bahset" 175 | 176 | #: templates/martor/bootstrap/toolbar.html:128 177 | #: templates/martor/semantic/toolbar.html:93 178 | msgid "Full Screen" 179 | msgstr "Tam ekran" 180 | 181 | #: templates/martor/bootstrap/toolbar.html:139 182 | #: templates/martor/semantic/toolbar.html:99 183 | msgid "Markdown Guide (Help)" 184 | msgstr "Markdown Kılavuzu (Yardım)" 185 | 186 | #: templates/martor/semantic/guide.html:7 187 | #, fuzzy, python-format 188 | #| msgid "Copyright © Martor" 189 | msgid "" 190 | "Copyright © Martor" 192 | msgstr "Telif hakkı © Martor" 193 | 194 | #: templates/martor/semantic/guide.html:11 195 | #, fuzzy, python-format 196 | #| msgid "" 197 | #| "This editor is powered by Markdown. For full documentation, click here." 199 | msgid "" 200 | "This editor is powered by Markdown. For full\n" 201 | " documentation, click here." 203 | msgstr "" 204 | "Bu editör Markdown tarafından desteklenmektedir. Bütün dokümantasyon için, " 205 | "buraya tıkla." 206 | 207 | #: views.py:19 views.py:37 views.py:38 208 | msgid "Invalid request!" 209 | msgstr "Geçersiz istek!" 210 | 211 | #: views.py:74 212 | #, python-format 213 | msgid "No users registered as `%(username)s` or user is unactived." 214 | msgstr "`%(username)s` kullanıcı kayıtlı değil veya aktif değil" 215 | 216 | #: views.py:79 217 | msgid "Validation Failed for field `username`" 218 | msgstr "`kullanıcı adı` alanı için doğrulama başarısız" 219 | -------------------------------------------------------------------------------- /martor_demo/app/templates/bootstrap/test_markdownify.html: -------------------------------------------------------------------------------- 1 | {% extends "bootstrap/base.html" %} 2 | {% load static %} 3 | {% load martortags %} 4 | {% block title %}Test Markdownify :: {{ block.super }}{% endblock %} 5 | 6 | {% block css %} 7 | 8 | 9 | 140 | {% endblock %} 141 | 142 | {% block content %} 143 |
144 |

Markdown Preview

145 |

See how your markdown content is beautifully rendered with Martor

146 |
147 | 148 |
149 |
Post Title
150 |
151 |

{{ post.title }}

152 |
153 |
154 | 155 |
156 |
Description
157 |
158 | {{ post.description|safe_markdown }} 159 |
160 |
161 | 162 |
163 |
164 |
{{ post.title|length }}
165 |
Title Characters
166 |
167 |
168 |
{{ post.description|length }}
169 |
Description Characters
170 |
171 |
172 |
{{ post.description|wordcount }}
173 |
Words
174 |
175 |
176 |
{{ post.description|truncatewords:10|length }}
177 |
Preview Length
178 |
179 |
180 | 181 | 192 | {% endblock %} 193 | 194 | {% block js %} 195 | 196 | 257 | {% endblock %} 258 | -------------------------------------------------------------------------------- /martor/templates/martor/bootstrap/guide.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | 3 | 208 | -------------------------------------------------------------------------------- /martor/static/martor/css/martor.semantic.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Name : Martor v1.7.16 3 | * Created by : Agus Makmun (Summon Agus) 4 | * Release date : 01-Nov-2025 5 | * License : GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 6 | * Repository : https://github.com/agusmakmun/django-markdown-editor 7 | * CSS Minifier : https://www.minifier.org 8 | **/ 9 | .submit-row a{box-sizing:content-box}body.overflow{overflow:hidden!important}.section-martor::-webkit-scrollbar-track{-webkit-box-shadow:inset 0 0 6px rgb(0 0 0 / .3);border-radius:10px;background-color:#F5F5F5}.section-martor::-webkit-scrollbar{height:6px;width:6px;background-color:#F5F5F5}.section-martor::-webkit-scrollbar-thumb{border-radius:10px;-webkit-box-shadow:inset 0 0 6px rgb(0 0 0 / .3);background-color:#555}.ace_scrollbar-v{cursor:ns-resize}.martor-toolbar{padding:0 .85714286em!important;padding-bottom:4px!important}.martor-toolbar.ui.icon.markdown-image-upload{position:relative;overflow:hidden}.martor-toolbar.ui.icon.markdown-image-upload input[type=file]{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:25px;padding:0;padding-left:35px;text-align:right;filter:alpha(opacity=0);opacity:0;outline:none;cursor:inherit;display:block}.emoji-loader-init{min-height:200px!important}.insert-emoji{cursor:pointer}.main-martor-fullscreen{background:#fff;position:fixed;z-index:9999;max-height:100%;height:100%;width:100%;margin:0;left:0;top:0}.main-martor-fullscreen.fields.martor-toolbar{border-bottom:1px solid #ddd;margin:0}.main-martor-fullscreen.section-martor{height:90%;position:relative}.main-martor-fullscreen.martor-field,.main-martor-fullscreen.martor-preview{min-height:95vh}.marked-emoji,div.martor-preview.marked-emoji{max-width:20px}div.martor-preview{font-size:14px;line-height:1.6}div.martor-preview>*:first-child{margin-top:0!important}div.martor-preview>*:last-child{margin-bottom:20px!important}div.martor-preview a.absent{color:#c00}div.martor-preview a.anchor{display:block;padding-left:30px;margin-left:-30px;cursor:pointer;position:absolute;top:0;left:0;bottom:0}div.martor-preview h1,div.martor-preview h2,div.martor-preview h3,div.martor-preview h4,div.martor-preview h5,div.martor-preview h6{margin:20px 0 10px;padding:0;font-weight:bold!important;-webkit-font-smoothing:antialiased;cursor:text;position:relative;background:none}div.martor-preview h1:hover a.anchor,div.martor-preview h2:hover a.anchor,div.martor-preview h3:hover a.anchor,div.martor-preview h4:hover a.anchor,div.martor-preview h5:hover a.anchor,div.martor-preview h6:hover a.anchor{text-decoration:none;line-height:1;padding-left:0;margin-left:-22px;top:15%}div.martor-preview h1 tt,div.martor-preview h1 code,div.martor-preview h2 tt,div.martor-preview h2 code,div.martor-preview h3 tt,div.martor-preview h3 code,div.martor-preview h4 tt,div.martor-preview h4 code,div.martor-preview h5 tt,div.martor-preview h5 code,div.martor-preview h6 tt,div.martor-preview h6 code{font-size:inherit}div.martor-preview h1{font-size:28px!important;color:#000!important}div.martor-preview h2{font-size:24px!important;color:#000!important}div.martor-preview h3{font-size:18px!important}div.martor-preview h4{font-size:16px!important}div.martor-preview h5{font-size:14px!important;text-transform:none!important}div.martor-preview h6{color:#777;font-size:14px}div.martor-preview p,div.martor-preview blockquote,div.martor-preview ul,div.martor-preview ol,div.martor-preview dl,div.martor-preview table,div.martor-preview pre{margin:15px 0}div.martor-preview hr{background:#fff0 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAYAAACtBE5DAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OENDRjNBN0E2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OENDRjNBN0I2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4Q0NGM0E3ODY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4Q0NGM0E3OTY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqqezsUAAAAfSURBVHjaYmRABcYwBiM2QSA4y4hNEKYDQxAEAAIMAHNGAzhkPOlYAAAAAElFTkSuQmCC)repeat-x 0 0;border:0 none;color:#ccc;height:4px;padding:0}div.martor-preview>h2:first-child,div.martor-preview>h1:first-child,div.martor-preview>h1:first-child+h2,div.martor-preview>h3:first-child,div.martor-preview>h4:first-child,div.martor-preview>h5:first-child,div.martor-preview>h6:first-child{margin-top:0;padding-top:0}div.martor-preview a:first-child h1,div.martor-preview a:first-child h2,div.martor-preview a:first-child h3,div.martor-preview a:first-child h4,div.martor-preview a:first-child h5,div.martor-preview a:first-child h6{margin-top:0;padding-top:0}div.martor-preview h1+p,div.martor-preview h2+p,div.martor-preview h3+p,div.martor-preview h4+p,div.martor-preview h5+p,div.martor-preview h6+p{margin-top:0}div.martor-preview li p.first{display:inline-block}div.martor-preview ul li{list-style:disc}div.martor-preview ul,div.martor-preview ol{padding-left:30px}div.martor-preview ul.no-list,div.martor-preview ol.no-list{list-style-type:none;padding:0}div.martor-preview ul li>:first-child,div.martor-preview ul li ul:first-of-type,div.martor-preview ol li>:first-child,div.martor-preview ol li ul:first-of-type{margin-top:0}div.martor-preview ul ul,div.martor-preview ul ol,div.martor-preview ol ol,div.martor-preview ol ul{margin-bottom:0}div.martor-preview dl{padding:0}div.martor-preview dl dt{font-size:14px;font-weight:700;font-style:italic;padding:0;margin:15px 0 5px}div.martor-preview dl dt:first-child{padding:0}div.martor-preview dl dt>:first-child{margin-top:0}div.martor-preview dl dt>:last-child{margin-bottom:0}div.martor-preview dl dd{margin:0 0 15px;padding:0 15px}div.martor-preview dl dd>:first-child{margin-top:0}div.martor-preview dl dd>:last-child{margin-bottom:0}div.martor-preview blockquote{border-left:4px solid #DDD;padding:5px 15px;color:#777;background-color:#fff}div.martor-preview blockquote>:first-child{margin-top:0}div.martor-preview blockquote>:last-child{margin-bottom:0}div.martor-preview table th{font-weight:700}div.martor-preview table th,div.martor-preview table td{border:1px solid #ccc;padding:6px 13px}div.martor-preview table tr{border-top:1px solid #ccc;background-color:#fff}div.martor-preview table tr:nth-child(2n){background-color:#f8f8f8}div.martor-preview img,div.martor-preview p img{max-width:100%;-moz-box-sizing:border-box;box-sizing:border-box}div.martor-preview span.frame{display:block;overflow:hidden}div.martor-preview span.frame>span{border:1px solid #ddd;display:block;float:left;overflow:hidden;margin:13px 0 0;padding:7px;width:auto}div.martor-preview span.frame span img{display:block;float:left}div.martor-preview span.frame span span{clear:both;color:#333;display:block;padding:5px 0 0}div.martor-preview span.align-center{display:block;overflow:hidden;clear:both}div.martor-preview span.align-center>span{display:block;overflow:hidden;margin:13px auto 0;text-align:center}div.martor-preview span.align-center span img{margin:0 auto;text-align:center}div.martor-preview span.align-right{display:block;overflow:hidden;clear:both}div.martor-preview span.align-right>span{display:block;overflow:hidden;margin:13px 0 0;text-align:right}div.martor-preview span.align-right span img{margin:0;text-align:right}div.martor-preview span.float-left{display:block;margin-right:13px;overflow:hidden;float:left}div.martor-preview span.float-left span{margin:13px 0 0}div.martor-preview span.float-right{display:block;margin-left:13px;overflow:hidden;float:right}div.martor-preview span.float-right>span{display:block;overflow:hidden;margin:13px auto 0;text-align:right}div.martor-preview code,div.martor-preview tt{margin:0 2px;padding:0 5px;border:1px solid #eaeaea;background-color:#f8f8f8;border-radius:3px}div.martor-preview code{white-space:nowrap}div.martor-preview pre>code{margin:0;padding:0;white-space:pre;border:none;background:#fff0}div.martor-preview.highlight pre,div.martor-preview pre{border:1px solid #f0f0f0;padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#f6f8fa;border-radius:3px}div.martor-preview pre code,div.martor-preview pre tt{margin:0;padding:0;background-color:#fff0;border:none}.section-martor div[data-tab="editor-tab-description"]{padding:0!important}.martor{height:500px;max-height:500px}.martor-field{width:100%;height:250px;min-height:100px;resize:vertical;border-right:1px solid #ccc;border-bottom:1px solid #ccc}div.martor-preview{padding:1rem;overflow:auto;background:#F9F9F9}div.martor-preview-stale{background:repeating-linear-gradient(-45deg,#fff,#fff 10px,#f8f8f8 10px,#f8f8f8 20px)!important}.icon.expand-editor{position:absolute;bottom:.8em;right:0}.no-border{border:none!important}div.enable-living.martor-preview{display:block!important;opacity:unset!important;border:1px solid #efefef!important;border-radius:.28571429rem!important}div.enable-living.tab-martor-menu a.item{display:none!important} 10 | -------------------------------------------------------------------------------- /martor/static/plugins/js/emojis.min.js: -------------------------------------------------------------------------------- 1 | /* Added Manually by: Summon Agus */ 2 | var emojis = [':yellow_heart:', ':blue_heart:', ':purple_heart:', ':heart:', ':green_heart:', ':broken_heart:', ':heartbeat:', ':heartpulse:', ':two_hearts:', ':revolving_hearts:', ':cupid:', ':sparkling_heart:', ':sparkles:', ':star:', ':star2:', ':dizzy:', ':boom:', ':collision:', ':anger:', ':exclamation:', ':question:', ':grey_exclamation:', ':grey_question:', ':zzz:', ':dash:', ':sweat_drops:', ':notes:', ':musical_note:', ':fire:', ':thumbsup:', ':-1:', ':thumbsdown:', ':ok_hand:', ':punch:', ':facepunch:', ':fist:', ':v:', ':wave:', ':hand:', ':raised_hand:', ':open_hands:', ':point_up:', ':point_down:', ':point_left:', ':point_right:', ':raised_hands:', ':pray:', ':point_up_2:', ':clap:', ':muscle:', ':nail_care:', ':sunny:', ':umbrella:', ':cloud:', ':snowflake:', ':zap:', ':cyclone:', ':foggy:', ':ocean:', ':bouquet:', ':cherry_blossom:', ':tulip:', ':four_leaf_clover:', ':rose:', ':sunflower:', ':hibiscus:', ':maple_leaf:', ':leaves:', ':fallen_leaf:', ':herb:', ':mushroom:', ':cactus:', ':palm_tree:', ':evergreen_tree:', ':deciduous_tree:', ':chestnut:', ':seedling:', ':blossom:', ':ear_of_rice:', ':shell:', ':globe_with_meridians:', ':new_moon:', ':waxing_crescent_moon:', ':first_quarter_moon:', ':waxing_gibbous_moon:', ':full_moon:', ':waning_gibbous_moon:', ':last_quarter_moon:', ':waning_crescent_moon:', ':last_quarter_moon_with_face:', ':first_quarter_moon_with_face:', ':crescent_moon:', ':earth_africa:', ':earth_americas:', ':earth_asia:', ':volcano:', ':milky_way:', ':partly_sunny:', ':bamboo:', ':gift_heart:', ':school_satchel:', ':mortar_board:', ':flags:', ':fireworks:', ':sparkler:', ':wind_chime:', ':rice_scene:', ':gift:', ':bell:', ':no_bell:', ':tanabata_tree:', ':tada:', ':confetti_ball:', ':balloon:', ':crystal_ball:', ':cd:', ':dvd:', ':floppy_disk:', ':camera:', ':video_camera:', ':movie_camera:', ':computer:', ':tv:', ':iphone:', ':phone:', ':telephone:', ':telephone_receiver:', ':pager:', ':fax:', ':minidisc:', ':vhs:', ':sound:', ':speaker:', ':mute:', ':loudspeaker:', ':mega:', ':hourglass:', ':hourglass_flowing_sand:', ':alarm_clock:', ':watch:', ':radio:', ':satellite:', ':loop:', ':mag:', ':mag_right:', ':unlock:', ':lock:', ':lock_with_ink_pen:', ':closed_lock_with_key:', ':key:', ':bulb:', ':flashlight:', ':high_brightness:', ':low_brightness:', ':electric_plug:', ':battery:', ':calling:', ':email:', ':mailbox:', ':postbox:', ':bath:', ':bathtub:', ':shower:', ':toilet:', ':wrench:', ':nut_and_bolt:', ':hammer:', ':seat:', ':moneybag:', ':yen:', ':dollar:', ':pound:', ':euro:', ':credit_card:', ':money_with_wings:', ':e-mail:', ':inbox_tray:', ':outbox_tray:', ':envelope:', ':incoming_envelope:', ':postal_horn:', ':mailbox_closed:', ':mailbox_with_mail:', ':mailbox_with_no_mail:', ':package:', ':door:', ':smoking:', ':bomb:', ':gun:', ':hocho:', ':pill:', ':syringe:', ':page_facing_up:', ':page_with_curl:', ':bookmark_tabs:', ':bar_chart:', ':chart_with_upwards_trend:', ':chart_with_downwards_trend:', ':scroll:', ':clipboard:', ':calendar:', ':date:', ':card_index:', ':file_folder:', ':open_file_folder:', ':scissors:', ':pushpin:', ':paperclip:', ':black_nib:', ':pencil2:', ':straight_ruler:', ':triangular_ruler:', ':closed_book:', ':green_book:', ':blue_book:', ':orange_book:', ':notebook:', ':notebook_with_decorative_cover:', ':ledger:', ':books:', ':bookmark:', ':name_badge:', ':microscope:', ':telescope:', ':newspaper:', ':football:', ':basketball:', ':soccer:', ':baseball:', ':tennis:', ':8ball:', ':rugby_football:', ':bowling:', ':golf:', ':gem:', ':ring:', ':trophy:', ':musical_score:', ':musical_keyboard:', ':violin:', ':video_game:', ':flower_playing_cards:', ':game_die:', ':dart:', ':clapper:', ':memo:', ':pencil:', ':book:', ':art:', ':microphone:', ':headphones:', ':trumpet:', ':saxophone:', ':guitar:', ':shoe:', ':sandal:', ':high_heel:', ':lipstick:', ':boot:', ':shirt:', ':tshirt:', ':necktie:', ':womans_clothes:', ':dress:', ':running_shirt_with_sash:', ':jeans:', ':kimono:', ':bikini:', ':ribbon:', ':tophat:', ':crown:', ':womans_hat:', ':mans_shoe:', ':closed_umbrella:', ':briefcase:', ':handbag:', ':pouch:', ':purse:', ':eyeglasses:', ':fishing_pole_and_fish:', ':coffee:', ':tea:', ':sake:', ':baby_bottle:', ':beer:', ':beers:', ':cocktail:', ':tropical_drink:', ':wine_glass:', ':fork_and_knife:', ':pizza:', ':hamburger:', ':fries:', ':poultry_leg:', ':meat_on_bone:', ':spaghetti:', ':curry:', ':fried_shrimp:', ':bento:', ':sushi:', ':fish_cake:', ':rice_ball:', ':rice_cracker:', ':rice:', ':ramen:', ':stew:', ':oden:', ':dango:', ':egg:', ':bread:', ':doughnut:', ':custard:', ':icecream:', ':ice_cream:', ':shaved_ice:', ':birthday:', ':cake:', ':cookie:', ':chocolate_bar:', ':candy:', ':lollipop:', ':honey_pot:', ':apple:', ':green_apple:', ':tangerine:', ':lemon:', ':cherries:', ':grapes:', ':watermelon:', ':strawberry:', ':peach:', ':melon:', ':banana:', ':pear:', ':pineapple:', ':sweet_potato:', ':eggplant:', ':tomato:', ':corn:', ':house:', ':house_with_garden:', ':school:', ':office:', ':post_office:', ':hospital:', ':bank:', ':convenience_store:', ':love_hotel:', ':hotel:', ':wedding:', ':church:', ':department_store:', ':european_post_office:', ':city_sunrise:', ':city_sunset:', ':japanese_castle:', ':european_castle:', ':tent:', ':factory:', ':tokyo_tower:', ':japan:', ':mount_fuji:', ':sunrise_over_mountains:', ':sunrise:', ':stars:', ':statue_of_liberty:', ':bridge_at_night:', ':carousel_horse:', ':rainbow:', ':ferris_wheel:', ':fountain:', ':roller_coaster:', ':ship:', ':speedboat:', ':boat:', ':sailboat:', ':rowboat:', ':anchor:', ':rocket:', ':airplane:', ':helicopter:', ':steam_locomotive:', ':tram:', ':mountain_railway:', ':bike:', ':aerial_tramway:', ':suspension_railway:', ':mountain_cableway:', ':tractor:', ':blue_car:', ':oncoming_automobile:', ':car:', ':red_car:', ':taxi:', ':oncoming_taxi:', ':articulated_lorry:', ':bus:', ':oncoming_bus:', ':rotating_light:', ':police_car:', ':oncoming_police_car:', ':fire_engine:', ':ambulance:', ':minibus:', ':truck:', ':train:', ':station:', ':train2:', ':bullettrain_front:', ':bullettrain_side:', ':light_rail:', ':monorail:', ':railway_car:', ':trolleybus:', ':ticket:', ':fuelpump:', ':vertical_traffic_light:', ':traffic_light:', ':warning:', ':construction:', ':beginner:', ':atm:', ':slot_machine:', ':busstop:', ':barber:', ':hotsprings:', ':checkered_flag:', ':crossed_flags:', ':izakaya_lantern:', ':moyai:', ':circus_tent:', ':performing_arts:', ':round_pushpin:', ':triangular_flag_on_post:', ':jp:', ':kr:', ':cn:', ':us:', ':fr:', ':es:', ':it:', ':ru:', ':gb:', ':uk:', ':de:', ':one:', ':two:', ':three:', ':four:', ':five:', ':six:', ':seven:', ':eight:', ':nine:', ':keycap_ten:', ':1234:', ':zero:', ':hash:', ':symbols:', ':arrow_backward:', ':arrow_down:', ':arrow_forward:', ':arrow_left:', ':capital_abcd:', ':abcd:', ':abc:', ':arrow_lower_left:', ':arrow_lower_right:', ':arrow_right:', ':arrow_up:', ':arrow_upper_left:', ':arrow_upper_right:', ':arrow_double_down:', ':arrow_double_up:', ':arrow_down_small:', ':arrow_heading_down:', ':arrow_heading_up:', ':leftwards_arrow_with_hook:', ':arrow_right_hook:', ':left_right_arrow:', ':arrow_up_down:', ':arrow_up_small:', ':arrows_clockwise:', ':arrows_counterclockwise:', ':rewind:', ':fast_forward:', ':information_source:', ':ok:', ':twisted_rightwards_arrows:', ':repeat:', ':repeat_one:', ':new:', ':top:', ':up:', ':cool:', ':free:', ':ng:', ':cinema:', ':koko:', ':signal_strength:', ':u5272:', ':u5408:', ':u55b6:', ':u6307:', ':u6708:', ':u6709:', ':u6e80:', ':u7121:', ':u7533:', ':u7a7a:', ':u7981:', ':sa:', ':restroom:', ':mens:', ':womens:', ':baby_symbol:', ':no_smoking:', ':parking:', ':wheelchair:', ':metro:', ':baggage_claim:', ':accept:', ':wc:', ':potable_water:', ':put_litter_in_its_place:', ':secret:', ':congratulations:', ':m:', ':passport_control:', ':left_luggage:', ':customs:', ':ideograph_advantage:', ':cl:', ':sos:', ':id:', ':no_entry_sign:', ':underage:', ':no_mobile_phones:', ':do_not_litter:', ':non-potable_water:', ':no_bicycles:', ':no_pedestrians:', ':children_crossing:', ':no_entry:', ':eight_spoked_asterisk:', ':sparkle:', ':eight_pointed_black_star:', ':heart_decoration:', ':vs:', ':vibration_mode:', ':mobile_phone_off:', ':chart:', ':currency_exchange:', ':aries:', ':taurus:', ':gemini:', ':cancer:', ':leo:', ':virgo:', ':libra:', ':scorpius:', ':sagittarius:', ':capricorn:', ':aquarius:', ':pisces:', ':ophiuchus:', ':six_pointed_star:', ':negative_squared_cross_mark:', ':a:', ':b:', ':ab:', ':o2:', ':diamond_shape_with_a_dot_inside:', ':recycle:', ':end:', ':back:', ':on:', ':soon:', ':clock1:', ':clock130:', ':clock10:', ':clock1030:', ':clock11:', ':clock1130:', ':clock12:', ':clock1230:', ':clock2:', ':clock230:', ':clock3:', ':clock330:', ':clock4:', ':clock430:', ':clock5:', ':clock530:', ':clock6:', ':clock630:', ':clock7:', ':clock730:', ':clock8:', ':clock830:', ':clock9:', ':clock930:', ':heavy_dollar_sign:', ':copyright:', ':registered:', ':tm:', ':x:', ':heavy_exclamation_mark:', ':bangbang:', ':interrobang:', ':o:', ':heavy_multiplication_x:', ':heavy_plus_sign:', ':heavy_minus_sign:', ':heavy_division_sign:', ':white_flower:', ':100:', ':heavy_check_mark:', ':ballot_box_with_check:', ':radio_button:', ':link:', ':curly_loop:', ':wavy_dash:', ':part_alternation_mark:', ':trident:', ':black_small_square:', ':white_small_square:', ':black_medium_small_square:', ':white_medium_small_square:', ':black_medium_square:', ':white_medium_square:', ':white_large_square:', ':white_check_mark:', ':black_square_button:', ':white_square_button:', ':black_circle:', ':white_circle:', ':red_circle:', ':large_blue_circle:', ':large_blue_diamond:', ':large_orange_diamond:', ':small_blue_diamond:', ':small_orange_diamond:', ':small_red_triangle:', ':small_red_triangle_down:'] 3 | --------------------------------------------------------------------------------