A main/migrations/0118_user_markdown_link_paste_on_and_more.py => main/migrations/0118_user_markdown_link_paste_on_and_more.py +23 -0
@@ 0,0 1,23 @@
+# Generated by Django 5.2.5 on 2025-09-17 20:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('main', '0117_alter_reallysimplelicensing_license_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='markdown_link_paste_on',
+ field=models.BooleanField(default=False, help_text='Enable/disable automatic markdown link formatting on paste.', verbose_name='Auto Markdown link formatting'),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='subscribe_note',
+ field=models.CharField(blank=True, default='Subscribe via [RSS](/rss/) / [Atom](/atom/) / [via Email](/newsletter/).', help_text='Default: Subscribe via [RSS](/rss/) / [Atom](/atom/) / [via Email](/newsletter/).', max_length=350, null=True),
+ ),
+ ]
M main/models.py => main/models.py +5 -0
@@ 156,6 156,11 @@ class User(AbstractUser):
verbose_name="Webring next URL",
help_text="URL for your webring's next website.",
)
+ markdown_link_paste_on = models.BooleanField(
+ default=False,
+ help_text="Enable/disable automatic markdown link formatting on paste.",
+ verbose_name="Auto Markdown link formatting",
+ )
# billing
stripe_customer_id = models.CharField(max_length=100, blank=True, null=True)
A main/templates/assets/markdown-paste-link.js => main/templates/assets/markdown-paste-link.js +30 -0
@@ 0,0 1,30 @@
+// get body element, used for paste into it
+var bodyElem = document.querySelector('textarea[name="body"]');
+
+function formatOnPaste(event) {
+ const clipboardData = event.clipboardData || window.clipboardData;
+ const pastedData = clipboardData.getData('text');
+
+ const bodyElem = document.querySelector('textarea[name="body"]');
+
+ const start = bodyElem.selectionStart;
+ const end = bodyElem.selectionEnd;
+
+ if (start !== end) {
+ event.preventDefault(); // Stop the default paste
+
+ const selectedText = bodyElem.value.substring(start, end);
+ const before = bodyElem.value.substring(0, start);
+ const after = bodyElem.value.substring(end);
+
+ const markdownLink = `[${selectedText}](${pastedData})`;
+
+ bodyElem.value = before + markdownLink + after;
+
+ // Move cursor after inserted markdown
+ const newCursorPosition = before.length + markdownLink.length;
+ bodyElem.setSelectionRange(newCursorPosition, newCursorPosition);
+ }
+}
+
+bodyElem.addEventListener('paste', formatOnPaste);<
\ No newline at end of file
M main/templates/main/page_form.html => main/templates/main/page_form.html +3 -1
@@ 64,7 64,9 @@
<script>
// when page loads, focus on title
document.querySelector('input[name="title"]').focus();
-
+ {% if request.user.markdown_link_paste_on %}
+ {% include "assets/markdown-paste-link.js" %}
+ {% endif %}
{% include "assets/drag-and-drop-upload.js" %}
</script>
{% endblock scripts %}
M main/templates/main/post_form.html => main/templates/main/post_form.html +3 -1
@@ 82,7 82,9 @@
<script>
{% include "assets/drag-and-drop-upload.js" %}
{% include "assets/make-draft-button.js" %}
-
+ {% if request.user.markdown_link_paste_on %}
+ {% include "assets/markdown-paste-link.js" %}
+ {% endif %}
{% if request.user.post_backups_on %}
{% include "assets/save-snapshot.js" %}
{% endif %}
M main/tests/test_users.py => main/tests/test_users.py +21 -0
@@ 289,3 289,24 @@ class UserDomainCheckTestCase(TestCase):
def test_domain_unknown(self):
response = self.client.get(reverse("domain_check") + "?domain=randomdomain.com")
self.assertEqual(response.status_code, 403)
+
+class UserMarkdownLinkOnPaste(TestCase):
+ def setUp(self):
+ self.user = models.User.objects.create(username="alice")
+ self.client.force_login(self.user)
+
+ def test_markdown_link_turned_on(self):
+ self.user.markdown_link_paste_on = True
+ self.user.save()
+ response = self.client.get(
+ reverse("post_create"),
+ )
+ self.assertContains(response, "formatOnPaste")
+
+ def test_markdown_link_turned_off(self):
+ self.user.markdown_link_turned_on = False
+ self.user.save()
+ response = self.client.get(
+ reverse("post_create"),
+ )
+ self.assertNotContains(response, "formatOnPaste")<
\ No newline at end of file
M main/views/general.py => main/views/general.py +1 -0
@@ 266,6 266,7 @@ class UserUpdate(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
"comments_on",
"notifications_on",
"mail_export_on",
+ "markdown_link_paste_on",
"redirect_domain",
"post_backups_on",
"show_posts_on_homepage",