From fc7325580cc27d7fed6338b7f9946ddf05cf1f49 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 30 Jan 2023 17:12:25 -0500 Subject: [PATCH] use mistune instead of python-markdown to keep leading unicode space(emsp) in markdown --- common/forms.py | 268 ------------------------------------------- journal/forms.py | 2 +- journal/models.py | 10 +- journal/renderers.py | 32 ++++++ journal/views.py | 1 + management/views.py | 2 - requirements.txt | 1 + 7 files changed, 41 insertions(+), 275 deletions(-) create mode 100644 journal/renderers.py diff --git a/common/forms.py b/common/forms.py index 23a3517d..89ac990d 100644 --- a/common/forms.py +++ b/common/forms.py @@ -7,101 +7,6 @@ from django.utils.translation import gettext_lazy as _ import json -class KeyValueInput(forms.Widget): - """ - Input widget for Json field - """ - - template_name = "widgets/hstore.html" - - def get_context(self, name, value, attrs): - context = super().get_context(name, value, attrs) - data = None - if context["widget"]["value"] is not None: - data = json.loads(context["widget"]["value"]) - context["widget"]["value"] = ( - [{p[0]: p[1]} for p in data.items()] if data else [] - ) - return context - - class Media: - js = ("js/key_value_input.js",) - - -class HstoreInput(forms.Widget): - """ - Input widget for Hstore field - """ - - template_name = "widgets/hstore.html" - - def format_value(self, value): - """ - Return a value as it should appear when rendered in a template. - """ - if value == "" or value is None: - return None - if self.is_localized: - return formats.localize_input(value) - # do not return str - return value - - class Media: - js = ("js/key_value_input.js",) - - -class JSONField(forms.fields.JSONField): - widget = KeyValueInput - - def to_python(self, value): - if not value: - return None - j = {} - if isinstance(value, dict): - j = value - else: - pairs = json.loads("[" + value + "]") - if isinstance(pairs, dict): - j = pairs - else: - # list or tuple - for pair in pairs: - j = {**j, **pair} - return super().to_python(j) - - -class RadioBooleanField(forms.ChoiceField): - widget = forms.RadioSelect - - def to_python(self, value): - """Return a Python boolean object.""" - # Explicitly check for the string 'False', which is what a hidden field - # will submit for False. Also check for '0', since this is what - # RadioSelect will provide. Because bool("True") == bool('1') == True, - # we don't need to handle that explicitly. - if isinstance(value, str) and value.lower() in ("false", "0"): - value = False - else: - value = bool(value) - return value - - -class RatingValidator: - """empty value is not validated""" - - def __call__(self, value): - if not isinstance(value, int): - raise ValidationError( - _("%(value)s is not an integer"), - params={"value": value}, - ) - if not str(value) in [str(i) for i in range(0, 11)]: - raise ValidationError( - _("%(value)s is not an integer in range 1-10"), - params={"value": value}, - ) - - class PreviewImageInput(forms.FileInput): template_name = "widgets/image.html" @@ -120,176 +25,3 @@ class PreviewImageInput(forms.FileInput): Return whether value is considered to be initial value. """ return bool(value and getattr(value, "url", False)) - - -class TagInput(forms.TextInput): - """ - Dump tag queryset into tag list - """ - - template_name = "widgets/tag.html" - - def format_value(self, value): - if value == "" or value is None or len(value) == 0: - return "" - tag_list = [] - try: - tag_list = [t["content"] for t in value] - except TypeError: - tag_list = [t.content for t in value] - # return ','.join(tag_list) - return tag_list - - class Media: - css = {"all": ("lib/css/tag-input.css",)} - js = ("lib/js/tag-input.js",) - - -class TagField(forms.CharField): - """ - Split comma connected string into tag list - """ - - widget = TagInput - - def to_python(self, value): - value = super().to_python(value) - if not value: - return - return [t.strip() for t in value.split(",")] - - -class MultiSelect(forms.SelectMultiple): - template_name = "widgets/multi_select.html" - - class Media: - css = { - "all": ( - "https://cdn.jsdelivr.net/npm/multiple-select@1.5.2/dist/multiple-select.min.css", - ) - } - js = ( - "https://cdn.jsdelivr.net/npm/multiple-select@1.5.2/dist/multiple-select.min.js", - ) - - -class HstoreField(forms.CharField): - widget = HstoreInput - - def to_python(self, value): - if not value: - return None - # already in python types - if isinstance(value, list): - return value - pairs = json.loads("[" + value + "]") - return pairs - - -class DurationInput(forms.TextInput): - """ - HH:mm:ss input widget - """ - - input_type = "time" - - def get_context(self, name, value, attrs): - context = super().get_context(name, value, attrs) - # context['widget']['type'] = self.input_type - context["widget"]["attrs"]["step"] = "1" - return context - - def format_value(self, value): - """ - Given `value` is an integer in ms - """ - ms = value - if not ms: - return super().format_value(None) - x = ms // 1000 - seconds = x % 60 - x //= 60 - if x == 0: - return super().format_value(f"00:00:{seconds:0>2}") - minutes = x % 60 - x //= 60 - if x == 0: - return super().format_value(f"00:{minutes:0>2}:{seconds:0>2}") - hours = x % 24 - return super().format_value(f"{hours:0>2}:{minutes:0>2}:{seconds:0>2}") - - -class DurationField(forms.TimeField): - widget = DurationInput - - def to_python(self, value): - - # empty value - if value is None or value == "": - return - - # if value is integer in ms - if isinstance(value, int): - return value - - # if value is string in time format - h, m, s = value.split(":") - return (int(h) * 3600 + int(m) * 60 + int(s)) * 1000 - - -############################# -# Form -############################# -VISIBILITY_CHOICES = [ - (0, _("公开")), - (1, _("仅关注者")), - (2, _("仅自己")), -] - - -class MarkForm(forms.ModelForm): - id = forms.IntegerField(required=False, widget=forms.HiddenInput()) - share_to_mastodon = forms.BooleanField( - label=_("分享到联邦网络"), initial=True, required=False - ) - rating = forms.IntegerField( - label=_("评分"), - validators=[RatingValidator()], - widget=forms.HiddenInput(), - required=False, - ) - visibility = forms.TypedChoiceField( - label=_("可见性"), - initial=0, - coerce=int, - choices=VISIBILITY_CHOICES, - widget=forms.RadioSelect, - ) - tags = TagField( - required=False, - widget=TagInput(attrs={"placeholder": _("回车增加标签")}), - label=_("标签"), - ) - text = forms.CharField( - required=False, - widget=forms.Textarea( - attrs={"placeholder": _("最多只能写360字哦~"), "maxlength": 360} - ), - label=_("短评"), - ) - - -class ReviewForm(forms.ModelForm): - title = forms.CharField(label=_("标题")) - content = MarkdownxFormField(label=_("正文 (Markdown)")) - share_to_mastodon = forms.BooleanField( - label=_("分享到联邦网络"), initial=True, required=False - ) - id = forms.IntegerField(required=False, widget=forms.HiddenInput()) - visibility = forms.TypedChoiceField( - label=_("可见性"), - initial=0, - coerce=int, - choices=VISIBILITY_CHOICES, - widget=forms.RadioSelect, - ) diff --git a/journal/forms.py b/journal/forms.py index 0ad0612a..73ed22e2 100644 --- a/journal/forms.py +++ b/journal/forms.py @@ -18,7 +18,7 @@ class ReviewForm(forms.ModelForm): } title = forms.CharField(label=_("评论标题")) - body = MarkdownxFormField(label=_("评论正文 (Markdown)")) + body = MarkdownxFormField(label=_("评论正文 (Markdown)"), strip=False) share_to_mastodon = forms.BooleanField( label=_("分享到联邦网络"), initial=False, required=False ) diff --git a/journal/models.py b/journal/models.py index ab4dbbe9..c5979ba9 100644 --- a/journal/models.py +++ b/journal/models.py @@ -22,9 +22,11 @@ from django.utils.baseconv import base62 from django.db.models import Q from catalog.models import * from django.contrib.contenttypes.models import ContentType -from markdown import markdown +from .renderers import render_md from catalog.common import jsondata +from journal import renderers + _logger = logging.getLogger(__name__) @@ -211,7 +213,7 @@ class Review(Content): @property def html_content(self): - return markdown(self.body) + return render_md(self.body) @cached_property def rating_grade(self): @@ -692,12 +694,12 @@ class Collection(List): @property def html(self): - html = markdown(self.brief) + html = render_md(self.brief) return html @property def plain_description(self): - html = markdown(self.brief) + html = render_md(self.brief) return _RE_HTML_TAG.sub(" ", html) def is_featured_by_user(self, user): diff --git a/journal/renderers.py b/journal/renderers.py new file mode 100644 index 00000000..2aaa1ccb --- /dev/null +++ b/journal/renderers.py @@ -0,0 +1,32 @@ +import mistune +import re + + +MARKDOWNX_MARKDOWNIFY_FUNCTION = "journal.renderers.render_md" + +_mistune_plugins = [ + "url", + "strikethrough", + "footnotes", + "table", + "mark", + "superscript", + "subscript", + "math", + "spoiler", +] +_markdown = mistune.create_markdown(plugins=_mistune_plugins) + + +def render_md(s): + # s = "\n".join( + # [ + # re.sub(r"^(\u2003+)", lambda s: " " * len(s[0]), line) + # for line in s.split("\n") + # ] + # ) + return _markdown(s) + + +def render_text(s): + return mistune.html(s) diff --git a/journal/views.py b/journal/views.py index ff9972fd..1c8e8058 100644 --- a/journal/views.py +++ b/journal/views.py @@ -422,6 +422,7 @@ def review_edit(request, item_uuid, review_uuid=None): if review else ReviewForm(request.POST) ) + print(form.instance.body) if form.is_valid(): if not review: form.instance.owner = request.user diff --git a/management/views.py b/management/views.py index 4d4dd9c6..232a9bc7 100644 --- a/management/views.py +++ b/management/views.py @@ -6,8 +6,6 @@ from django.utils.decorators import method_decorator from django.contrib.auth.decorators import login_required, user_passes_test from django.views.generic import * from django.views.generic.edit import ModelFormMixin -from markdown import markdown -import re # https://docs.djangoproject.com/en/3.1/topics/class-based-views/intro/ diff --git a/requirements.txt b/requirements.txt index f332c27f..bfca33fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ dateparser +mistune @ git+https://github.com/alphatownsman/mistune.git@660b1c0100ecdd6cd6aca26a554be2606a67d67b django~=3.2.16 django-markdownx @ git+https://github.com/alphatownsman/django-markdownx.git@e69480c64ad9c5d0499f4a8625da78cf2bb7691b django-jsoneditor @ git+https://github.com/alphatownsman/django-jsoneditor.git@fa2ae41aeeb34447bd8a808a520e843c853fd16e