From 8f163a15779a355ee606daea3242a409e5ebd4e0 Mon Sep 17 00:00:00 2001 From: Francesco Bellini Date: Fri, 4 Oct 2024 01:35:13 +0200 Subject: [PATCH 1/2] Feat: API for markdown preview rendering POST /markdown calls the same render function as templates --- extensions/urls.py | 1 + extensions/views/api.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/extensions/urls.py b/extensions/urls.py index 46cfe55a..cd3ffa14 100644 --- a/extensions/urls.py +++ b/extensions/urls.py @@ -21,6 +21,7 @@ urlpatterns = [ name='api-internal', ), # Public API + path('markdown/', api.MarkdownRenderApi.as_view(), name='markdown-rendering'), path('api/v1/extensions/', api.ExtensionsAPIView.as_view(), name='api'), path( 'api/v1/extensions//versions/upload/', diff --git a/extensions/views/api.py b/extensions/views/api.py index 5eefa6ca..3aa90c6b 100644 --- a/extensions/views/api.py +++ b/extensions/views/api.py @@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError from django.db import transaction from django.http import JsonResponse from django.utils.decorators import method_decorator +from django.views import View from django.views.decorators.cache import cache_page from drf_spectacular.utils import OpenApiParameter, extend_schema from rest_framework.permissions import AllowAny @@ -13,6 +14,7 @@ from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from common.compare import is_in_version_range, version +from common.markdown import render as render_markdown from extensions.models import Extension, Platform from extensions.utils import clean_json_dictionary_from_optional_fields from files.forms import FileFormSkipAgreed @@ -278,3 +280,10 @@ def extensions_awaiting_review(request): } ) return JsonResponse(response, safe=False) + +class MarkdownRenderApi(View): + + def post(self, request, *args, **kwargs): + text = request.POST.get('text') + rendered_markdown = render_markdown(text) + return JsonResponse({'markdown': rendered_markdown}) -- 2.30.2 From 1877cbc9368d62267538af7e38e84aa3933448b6 Mon Sep 17 00:00:00 2001 From: Francesco Bellini Date: Fri, 4 Oct 2024 01:35:27 +0200 Subject: [PATCH 2/2] Feat: Markdown preview for markdown-supported fields - Add is_markdown_field template filter - Add tabs Write | Preview on top of the textarea - Add initMarkdownPreview function to handle tabs content and api fetch client-side --- .../static/common/scripts/markdown-preview.js | 78 +++++++++++++++++++ common/templates/common/components/field.html | 33 +++++++- common/templatetags/common.py | 4 + 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 common/static/common/scripts/markdown-preview.js diff --git a/common/static/common/scripts/markdown-preview.js b/common/static/common/scripts/markdown-preview.js new file mode 100644 index 00000000..84b60d88 --- /dev/null +++ b/common/static/common/scripts/markdown-preview.js @@ -0,0 +1,78 @@ +const activeTabCls = "is-active" + +function initMarkdownPreview(fieldName, texts) { + const field = document.querySelector(`textarea.form-control[name="${fieldName}"]`); + const markdown = document.querySelector(`#markdown-preview-${fieldName}`); + const tokenInput = document.querySelector(`input[name=csrfmiddlewaretoken]`) + const writeTab = document.querySelector(`#tab-write-${fieldName}`); + const previewTab = document.querySelector(`#tab-preview-${fieldName}`); + const write = document.querySelector(`#write-${fieldName}`); + const preview = document.querySelector(`#preview-${fieldName}`); + let lastText; // To avoid calling multiple times unchanged text + + function toggleActiveTab() { + writeTab.classList.toggle(activeTabCls); + previewTab.classList.toggle(activeTabCls); + } + + function opacity_75(text) { + return `${text}`; + } + + function nothingToPreview() { + markdown.innerHTML = opacity_75(texts.nothing_to_preview); + } + + function switchTabs(on, off) { + on.style.display = "block"; + off.style.display = "none"; + toggleActiveTab(); + } + + if (previewTab && field) { + previewTab.addEventListener("click", function (e) { + e.preventDefault(); + if (!previewTab.classList.contains(activeTabCls)) { + switchTabs(preview, write) + if (field.value !== lastText) { + markdown.innerHTML = opacity_75(texts.loading); + lastText = field.value; + if (field.value.trim()) { + fetch("/markdown/", { + method: "POST", + headers: { + "X-CSRFToken": tokenInput.value, + }, + body: new URLSearchParams({ + text: field.value, + }), + }) + .then((response) => response.json()) + .then((data) => { + const preview_markdown = data.markdown; + if (preview_markdown.trim()) { + markdown.innerHTML = preview_markdown; + } else { + nothingToPreview(); + } + }) + .catch((err) => { + lastText = undefined; + markdown.innerHTML = opacity_75(texts.error); + }); + } else { + nothingToPreview(); + } + } + } + }); + } + if (writeTab) { + writeTab.addEventListener("click", function (e) { + e.preventDefault(); + if (!writeTab.classList.contains(activeTabCls)) { + switchTabs(write, preview) + } + }); + } +} diff --git a/common/templates/common/components/field.html b/common/templates/common/components/field.html index 826cc098..4730591d 100644 --- a/common/templates/common/components/field.html +++ b/common/templates/common/components/field.html @@ -1,7 +1,8 @@ -{% load common %} +{% load i18n common %} {% spaceless %} {% with type=field.field.widget.input_type classes=classes|default:"" placeholder=placeholder|default:"" %} {% with field=field|remove_cols_rows|add_classes:classes|set_placeholder:placeholder %} + {% with is_markdown=field.name|is_markdown_field %} {% autoescape off %} {% firstof label field.label as label %} {% firstof help_text field.help_text as help_text %} @@ -27,6 +28,16 @@ {% if field.field.required or required %}*{% endif %} {% endif %} + + {% if is_markdown and not field.is_hidden %} + + + {% endif %} {% if icon %}
@@ -36,7 +47,9 @@ {{ field }}
{% else %} - {{ field }} +
+ {{ field }} +
{% endif %} {% endif %} @@ -47,6 +60,22 @@ {% if field.errors %}
{{ field.errors }}
{% endif %} + + {% block scripts %} + {% if is_markdown and not field.is_hidden %} + + {% endif %} + {% endblock scripts %} + + {% endwith %} {% endwith %} {% endwith %} {% endspaceless %} diff --git a/common/templatetags/common.py b/common/templatetags/common.py index 6eff034e..7a26af52 100644 --- a/common/templatetags/common.py +++ b/common/templatetags/common.py @@ -230,3 +230,7 @@ def to_int(value): return int(value) except (ValueError, TypeError): return 0 + +@register.filter +def is_markdown_field(name: str) -> bool: + return name in ['description', 'release_notes', 'message'] -- 2.30.2