From 9636789e6b28057605ffe49abba1cf7065c1d489 Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Mon, 20 Nov 2023 15:43:54 +0200 Subject: [PATCH 1/8] markdown content for books this is a temporary setup to for the law reader the books are uploaded in markdown and converted to html using pandoc --- Dockerfile | 2 +- peachjam/admin.py | 15 ++++++++++++++- peachjam/helpers.py | 23 +++++++++++++++++++++++ peachjam/models/journals_books.py | 2 ++ peachjam/settings.py | 16 ++++++++++++++++ peachjam/urls.py | 2 ++ pyproject.toml | 1 + 7 files changed, 59 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index acf2bed58..a5eaa30ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 # LibreOffice -RUN apt-get update && apt-get install -y libreoffice poppler-utils +RUN apt-get update && apt-get install -y libreoffice poppler-utils pandoc # Production-only dependencies RUN pip install psycopg2==2.9.3 gunicorn==20.1.0 diff --git a/peachjam/admin.py b/peachjam/admin.py index 5c0358cbe..e580c684c 100644 --- a/peachjam/admin.py +++ b/peachjam/admin.py @@ -22,6 +22,7 @@ from django.utils.translation import gettext_lazy from import_export.admin import ImportExportMixin as BaseImportExportMixin from languages_plus.models import Language +from martor.utils import markdownify from nonrelated_inlines.admin import NonrelatedTabularInline from treebeard.admin import TreeAdmin from treebeard.forms import MoveNodeForm, movenodeform_factory @@ -984,7 +985,19 @@ class GazetteAdmin(DocumentAdmin): @admin.register(Book) class BookAdmin(DocumentAdmin): - pass + fieldsets = copy.deepcopy(DocumentAdmin.fieldsets) + fieldsets[3][1]["fields"].insert(3, "content_markdown") + + def save_model(self, request, obj, form, change): + if "content_markdown" in form.changed_data: + obj.content_html = markdownify(form.cleaned_data["content_markdown"]) + + resp = super().save_model(request, obj, form, change) + + if "content_markdown" in form.changed_data: + obj.extract_citations() + + return resp @admin.register(Journal) diff --git a/peachjam/helpers.py b/peachjam/helpers.py index 6ce7cc8fe..d5ff40893 100644 --- a/peachjam/helpers.py +++ b/peachjam/helpers.py @@ -5,6 +5,7 @@ from datetime import datetime from functools import wraps +import martor.utils from django.utils.translation import get_language_from_request from languages_plus.models import Language @@ -78,3 +79,25 @@ def parse_utf8_html(html): parser = lxml.html.HTMLParser(encoding="utf-8") return lxml.html.fromstring(html, parser=parser) + + +def markdownify(text): + """Convert markdown text to html using pandoc on the commandline.""" + with tempfile.NamedTemporaryFile(suffix=".md") as inf: + with tempfile.NamedTemporaryFile(suffix=".html") as outf: + inf.write(text.encode("utf-8")) + inf.flush() + cmd = [ + "pandoc", + "--from=markdown", + "--to=html", + "--output", + outf.name, + inf.name, + ] + subprocess.run(cmd, check=True) + return outf.read().decode("utf-8") + + +# override martor's markownify to use pandoc, so that we get alpha-numbered list support +martor.utils.markdownify = markdownify diff --git a/peachjam/models/journals_books.py b/peachjam/models/journals_books.py index 66d78d5fb..c585a0738 100644 --- a/peachjam/models/journals_books.py +++ b/peachjam/models/journals_books.py @@ -1,10 +1,12 @@ from django.db import models +from martor.models import MartorField from peachjam.models import CoreDocument, DocumentNature class Book(CoreDocument): publisher = models.CharField(max_length=2048) + content_markdown = MartorField(blank=True, null=True) def pre_save(self): self.frbr_uri_doctype = "doc" diff --git a/peachjam/settings.py b/peachjam/settings.py index 0158d0138..af0e1dce4 100644 --- a/peachjam/settings.py +++ b/peachjam/settings.py @@ -75,6 +75,7 @@ "polymorphic", "drf_spectacular", "django_advanced_password_validation", + "martor", ] MIDDLEWARE = [ @@ -568,3 +569,18 @@ # Each item in the list should be a tuple of (Full name, email address). Example: # [('Someone', 'someone@example.com')] ADMINS = [] + +# django-markdown-editor +# https://github.com/agusmakmun/django-markdown-editor +MARTOR_UPLOAD_URL = "" +MARTOR_SEARCH_USERS_URL = "" +MARTOR_ENABLE_LABEL = True +MARTOR_ENABLE_CONFIGS = { + "emoji": "true", # to enable/disable emoji icons. + "imgur": "false", # to enable/disable imgur/custom uploader. + "mention": "false", # to enable/disable mention + "jquery": "true", # to include/revoke jquery (require for admin default django) + "living": "false", # to enable/disable live updates in preview + "spellcheck": "false", # to enable/disable spellcheck in form textareas + "hljs": "false", # to enable/disable hljs highlighting in preview +} diff --git a/peachjam/urls.py b/peachjam/urls.py index b50200e25..be0f9986a 100644 --- a/peachjam/urls.py +++ b/peachjam/urls.py @@ -253,6 +253,8 @@ DocumentProblemView.as_view(), name="document_problem", ), + # django-markdown-editor + path("martor/", include("martor.urls")), ] if settings.DEBUG: diff --git a/pyproject.toml b/pyproject.toml index 0b91b3e9e..f039ec1a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ dependencies = [ "google-auth-oauthlib>=0.5.1", "igraph>=0.10.4", "lxml>=4.8.0", + "martor>=1.6", "Pillow>=9.0.1", "pre-commit>=2.17.0", "python-magic>=0.4.25", From d72ac4d1e60350913a620d6b21a840bef125447d Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Mon, 20 Nov 2023 15:54:50 +0200 Subject: [PATCH 2/8] disable martor theme --- .../migrations/0110_book_content_markdown.py | 19 +++++++++++++++++++ peachjam/settings.py | 2 ++ 2 files changed, 21 insertions(+) create mode 100644 peachjam/migrations/0110_book_content_markdown.py diff --git a/peachjam/migrations/0110_book_content_markdown.py b/peachjam/migrations/0110_book_content_markdown.py new file mode 100644 index 000000000..0cdad35e5 --- /dev/null +++ b/peachjam/migrations/0110_book_content_markdown.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.20 on 2023-11-20 10:34 + +import martor.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("peachjam", "0109_add_overturned_and_upheld_predicates"), + ] + + operations = [ + migrations.AddField( + model_name="book", + name="content_markdown", + field=martor.models.MartorField(blank=True, null=True), + ), + ] diff --git a/peachjam/settings.py b/peachjam/settings.py index af0e1dce4..b5f814d9a 100644 --- a/peachjam/settings.py +++ b/peachjam/settings.py @@ -584,3 +584,5 @@ "spellcheck": "false", # to enable/disable spellcheck in form textareas "hljs": "false", # to enable/disable hljs highlighting in preview } +# disable the normal martor theme which pulls in another bootstrap version +MARTOR_ALTERNATIVE_CSS_FILE_THEME = "x" From 042a2679c3b56ddb42099852339d5f815393f267 Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Wed, 6 Dec 2023 15:01:10 +0200 Subject: [PATCH 3/8] add slightly customised martor.bootstrap.js and css otherwise there's a weird bug when editing in full screen --- .../static/martor/css/martor-admin.min.css | 1 + peachjam/static/martor/js/martor.bootstrap.js | 875 ++++++++++++++++++ 2 files changed, 876 insertions(+) create mode 100644 peachjam/static/martor/css/martor-admin.min.css create mode 100644 peachjam/static/martor/js/martor.bootstrap.js diff --git a/peachjam/static/martor/css/martor-admin.min.css b/peachjam/static/martor/css/martor-admin.min.css new file mode 100644 index 000000000..257a86e02 --- /dev/null +++ b/peachjam/static/martor/css/martor-admin.min.css @@ -0,0 +1 @@ +// deliberately empty to prevent the martor-admin.min.css file from martor from loading since it messes with our styles diff --git a/peachjam/static/martor/js/martor.bootstrap.js b/peachjam/static/martor/js/martor.bootstrap.js new file mode 100644 index 000000000..d8985a72c --- /dev/null +++ b/peachjam/static/martor/js/martor.bootstrap.js @@ -0,0 +1,875 @@ +/** + * Name : Martor v1.6.28 + * Created by : Agus Makmun (Summon Agus) + * Release date : 20-Jul-2023 + * License : GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 + * Repository : https://github.com/agusmakmun/django-markdown-editor +**/ + +(function ($) { + if (!$) { + $ = django.jQuery; + } + $.fn.martor = function () { + $('.martor').trigger('martor.init'); + + // CSRF code + var getCookie = function (name) { + var cookieValue = null; + var i = 0; + if (document.cookie && document.cookie !== '') { + var cookies = document.cookie.split(';'); + for (i; i < cookies.length; i++) { + var cookie = jQuery.trim(cookies[i]); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + }; + + // Each multiple editor fields + this.each(function (i, obj) { + var mainMartor = $(obj); + var field_name = mainMartor.data('field-name'); + var textareaId = $('#id_' + field_name); + var editorId = 'martor-' + field_name; + var editor = ace.edit(editorId); + var editorConfig = JSON.parse(textareaId.data('enable-configs').replace(/'/g, '"')); + + editor.setTheme('ace/theme/github'); + editor.getSession().setMode('ace/mode/markdown'); + editor.getSession().setUseWrapMode(true); + editor.$blockScrolling = Infinity; // prevents ace from logging annoying warnings + editor.renderer.setScrollMargin(10, 10); // set padding + editor.setAutoScrollEditorIntoView(true); + editor.setShowPrintMargin(false); + editor.setOptions({ + enableBasicAutocompletion: true, + enableSnippets: true, + enableLiveAutocompletion: true, + enableMultiselect: false + }); + + if (editorConfig.living == 'true') { + $(obj).addClass('enable-living'); + } + + var emojiWordCompleter = { + getCompletions: function (editor, session, pos, prefix, callback) { + var wordList = typeof (emojis) != "undefined" ? emojis : []; // from `atwho/emojis.min.js` + var obj = editor.getSession().getTokenAt(pos.row, pos.column.count); + if (typeof (obj.value) != "undefined") { + var curTokens = obj.value.split(/\s+/); + var lastToken = curTokens[curTokens.length - 1]; + + if (lastToken[0] == ':') { + callback(null, wordList.map(function (word) { + return { + caption: word, + value: word.replace(':', '') + ' ', + meta: 'emoji' // this should return as text only. + }; + })); + } + } + } + } + var mentionWordCompleter = { + getCompletions: function (editor, session, pos, prefix, callback) { + var obj = editor.getSession().getTokenAt(pos.row, pos.column.count); + if (typeof (obj.value) != "undefined") { + var curTokens = obj.value.split(/\s+/); + var lastToken = curTokens[curTokens.length - 1]; + + if (lastToken[0] == '@' && lastToken[1] == '[') { + username = lastToken.replace(/([\@\[/\]/])/g, ''); + $.ajax({ + url: textareaId.data('search-users-url'), + data: { + 'username': username, + 'csrfmiddlewaretoken': getCookie('csrftoken') + }, + success: function (data) { + if (data['status'] == 200) { + var wordList = []; + for (var i = 0; i < data['data'].length; i++) { + wordList.push(data['data'][i].username) + } + callback(null, wordList.map(function (word) { + return { + caption: word, + value: word, + meta: 'username' // this should return as text only. + }; + })); + } + }// end success + }); + } + } + } + } + // Set autocomplete for ace editor + if (editorConfig.mention === 'true') { + editor.completers = [emojiWordCompleter, mentionWordCompleter] + } else { + editor.completers = [emojiWordCompleter] + } + + // set css `display:none` fot this textarea. + textareaId.attr({ 'style': 'display:none' }); + + // assign all `field_name`, uses for a per-single editor. + $(obj).find('.martor-toolbar').find('.markdown-selector').attr({ 'data-field-name': field_name }); + $(obj).find('.upload-progress').attr({ 'data-field-name': field_name }); + $(obj).find('.modal-help-guide').attr({ 'data-field-name': field_name }); + $(obj).find('.modal-emoji').attr({ 'data-field-name': field_name }); + + // Set if editor has changed. + editor.on('change', function (evt) { + var value = editor.getValue(); + textareaId.val(value); + }); + + // resize the editor using `resizable.min.js` + $('#' + editorId).resizable({ + direction: 'bottom', + stop: function () { + editor.resize(); + } + }); + + // update the preview if this menu is clicked + var currentTab = $('.tab-pane#nav-preview-' + field_name); + var editorTabButton = $('.nav-link#nav-editor-tab-' + field_name); + var previewTabButton = $('.nav-link#nav-preview-tab-' + field_name); + var toolbarButtons = $(this).closest('.tab-martor-menu').find('.martor-toolbar') + + editorTabButton.click(function () { + // show the `.martor-toolbar` for this current editor if under preview. + $(this).closest('.tab-martor-menu').find('.martor-toolbar').show(); + }); + previewTabButton.click(function () { + $(this).closest('.tab-martor-menu').find('.martor-toolbar').hide(); + }); + + var refreshPreview = function () { + var value = textareaId.val(); + var form = new FormData(); + form.append('content', value); + form.append('csrfmiddlewaretoken', getCookie('csrftoken')); + currentTab.addClass('martor-preview-stale'); + + $.ajax({ + url: textareaId.data('markdownfy-url'), + type: 'POST', + data: form, + processData: false, + contentType: false, + success: function (response) { + if (response) { + currentTab.html(response).removeClass('martor-preview-stale'); + $(document).trigger('martor:preview', [currentTab]); + + if (editorConfig.hljs == 'true') { + $('pre').each(function (i, block) { + hljs.highlightBlock(block); + }); + } + } else { + currentTab.html('

Nothing to preview

'); + } + }, + error: function (response) { + console.log("error", response); + } + }); + }; + + // Refresh the preview unconditionally on first load. + window.onload = function () { + refreshPreview(); + }; + + if (editorConfig.living !== 'true') { + previewTabButton.click(function () { + // hide the `.martor-toolbar` for this current editor if under preview. + $(this).closest('.tab-martor-menu').find('.martor-toolbar').hide(); + refreshPreview(); + }); + } else { + editor.on('change', refreshPreview); + } + + if (editorConfig.spellcheck == 'true') { + try { + enable_spellcheck(editorId); + } catch (e) { + console.log("Spellcheck lib doesn't installed."); + } + } + + // win/linux: Ctrl+B, mac: Command+B + var markdownToBold = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, ' **** '); + editor.focus(); + editor.selection.moveTo(curpos.row, curpos.column + 3); + } else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '**' + text + '**'); + originalRange.end.column += 4; // this because injected from 4 `*` characters. + editor.focus(); + editor.selection.setSelectionRange(originalRange); + } + }; + // win/linux: Ctrl+I, mac: Command+I + var markdownToItalic = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, ' __ '); + editor.focus(); + editor.selection.moveTo(curpos.row, curpos.column + 2); + } else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '_' + text + '_'); + originalRange.end.column += 2; // this because injected from 2 `_` characters. + editor.focus(); + editor.selection.setSelectionRange(originalRange); + } + }; + // win/linux: Ctrl+Shift+U, mac: Command+Shift+U + var markdownToUnderscores = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, ' ++++ '); + editor.focus(); + editor.selection.moveTo(curpos.row, curpos.column + 3); + } else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '++' + text + '++'); + originalRange.end.column += 4; // this because injected from 4 `*` characters. + editor.focus(); + editor.selection.setSelectionRange(originalRange); + } + }; + // win/linux: Ctrl+Shift+S, mac: Command+Shift+S + var markdownToStrikethrough = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, ' ~~~~ '); + editor.focus(); + editor.selection.moveTo(curpos.row, curpos.column + 3); + } else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '~~' + text + '~~'); + originalRange.end.column += 4; // this because injected from 4 `*` characters. + editor.focus(); + editor.selection.setSelectionRange(originalRange); + } + }; + // win/linux: Ctrl+H, mac: Command+H + var markdownToHorizontal = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, '\n\n----------\n\n'); + editor.focus(); + editor.selection.moveTo(curpos.row + 4, curpos.column + 10); + } + else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '\n\n----------\n\n' + text); + editor.focus(); + editor.selection.moveTo( + originalRange.end.row + 4, + originalRange.end.column + 10 + ); + } + }; + // win/linux: Ctrl+Alt+1, mac: Command+Option+1 + var markdownToH1 = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, '\n\n# '); + editor.focus(); + editor.selection.moveTo(curpos.row + 2, curpos.column + 2); + } + else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '\n\n# ' + text + '\n'); + editor.focus(); + editor.selection.moveTo( + originalRange.end.row + 2, + originalRange.end.column + 2 + ); + } + }; + // win/linux: Ctrl+Alt+2, mac: Command+Option+2 + var markdownToH2 = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, '\n\n## '); + editor.focus(); + editor.selection.moveTo(curpos.row + 2, curpos.column + 3); + } + else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '\n\n## ' + text + '\n'); + editor.focus(); + editor.selection.moveTo( + originalRange.end.row + 2, + originalRange.end.column + 3 + ); + } + }; + // win/linux: Ctrl+Alt+3, mac: Command+Option+3 + var markdownToH3 = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, '\n\n### '); + editor.focus(); + editor.selection.moveTo(curpos.row + 2, curpos.column + 4); + } + else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '\n\n### ' + text + '\n'); + editor.focus(); + editor.selection.moveTo( + originalRange.end.row + 2, + originalRange.end.column + 4 + ); + } + }; + // win/linux: Ctrl+Alt+P, mac: Command+Option+P + var markdownToPre = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, '\n\n```\n\n```\n'); + editor.focus(); + editor.selection.moveTo(curpos.row + 3, curpos.column); + } + else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '\n\n```\n' + text + '\n```\n'); + editor.focus(); + editor.selection.moveTo( + originalRange.end.row + 3, + originalRange.end.column + 3 + ); + } + }; + // win/linux: Ctrl+Alt+C, mac: Command+Option+C + var markdownToCode = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, ' `` '); + editor.focus(); + editor.selection.moveTo(curpos.row, curpos.column + 2); + } else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '`' + text + '`'); + originalRange.end.column += 2; // this because injected from 2 `_` characters. + editor.focus(); + editor.selection.setSelectionRange(originalRange); + } + }; + // win/linux: Ctrl+Q, mac: Command+Shift+K + var markdownToBlockQuote = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, '\n\n> \n'); + editor.focus(); + editor.selection.moveTo(curpos.row + 2, curpos.column + 2); + } + else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '\n\n> ' + text + '\n'); + editor.focus(); + editor.selection.moveTo( + originalRange.end.row + 2, + originalRange.end.column + 2 + ); + } + }; + // win/linux: Ctrl+U, mac: Command+U + var markdownToUnorderedList = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, '\n\n* '); + editor.focus(); + editor.selection.moveTo(curpos.row + 2, curpos.column + 2); + } + else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '\n\n* ' + text); + editor.focus(); + editor.selection.moveTo( + originalRange.end.row + 2, + originalRange.end.column + 2 + ); + } + }; + // win/linux: Ctrl+Shift+O, mac: Command+Option+O + var markdownToOrderedList = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, '\n\n1. '); + editor.focus(); + editor.selection.moveTo(curpos.row + 2, curpos.column + 3); + } + else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '\n\n1. ' + text); + editor.focus(); + editor.selection.moveTo( + originalRange.end.row + 2, + originalRange.end.column + 3 + ); + } + }; + // win/linux: Ctrl+L, mac: Command+L + var markdownToLink = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, ' [](https://) '); + editor.focus(); + editor.selection.moveTo(curpos.row, curpos.column + 2); + } else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '[' + text + '](https://) '); + editor.focus(); + editor.selection.moveTo( + originalRange.end.row, + originalRange.end.column + 11 + ); + } + }; + // win/linux: Ctrl+Shift+I, mac: Command+Option+I + // or via upload: imageData={name:null, link:null} + var markdownToImageLink = function (editor, imageData) { + var originalRange = editor.getSelectionRange(); + if (typeof (imageData) === 'undefined') { + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, ' ![](https://) '); + editor.focus(); + editor.selection.moveTo(curpos.row, curpos.column + 3); + } else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '![' + text + '](https://) '); + editor.focus(); + editor.selection.moveTo( + originalRange.end.row, + originalRange.end.column + 12 + ); + } + } else { // this if use image upload to imgur. + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, '![' + imageData.name + '](' + imageData.link + ') '); + editor.focus(); + editor.selection.moveTo( + curpos.row, + curpos.column + imageData.name.length + 2 + ); + } + }; + // win/linux: Ctrl+M, mac: Command+M + var markdownToMention = function (editor) { + var originalRange = editor.getSelectionRange(); + if (editor.selection.isEmpty()) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, ' @[]'); + editor.focus(); + editor.selection.moveTo(curpos.row, curpos.column + 3); + } else { + var range = editor.getSelectionRange(); + var text = editor.session.getTextRange(range); + editor.session.replace(range, '@[' + text + ']'); + editor.focus(); + editor.selection.moveTo( + originalRange.end.row, + originalRange.end.column + 3 + ) + } + }; + // Insert Emoji to text editor: $('.insert-emoji').data('emoji-target') + var markdownToEmoji = function (editor, data_target) { + var curpos = editor.getCursorPosition(); + editor.session.insert(curpos, ' ' + data_target + ' '); + editor.focus(); + editor.selection.moveTo(curpos.row, curpos.column + data_target.length + 2); + }; + // Markdown Image Uploader auto insert to editor. + // with special insert, eg: ![avatar.png](i.imgur.com/DytfpTz.png) + var markdownToUploadImage = function (editor) { + var firstForm = $('#' + editorId).closest('form').get(0); + var field_name = editor.container.id.replace('martor-', ''); + var form = new FormData(firstForm); + form.append('csrfmiddlewaretoken', getCookie('csrftoken')); + + $.ajax({ + url: textareaId.data('upload-url'), + type: 'POST', + data: form, + async: true, + cache: false, + contentType: false, + enctype: 'multipart/form-data', + processData: false, + beforeSend: function () { + console.log('Uploading...'); + $('.upload-progress[data-field-name=' + field_name + ']').show(); + }, + success: function (response) { + $('.upload-progress[data-field-name=' + field_name + ']').hide(); + if (response.status == 200) { + console.log(response); + markdownToImageLink( + editor = editor, + imageData = { name: response.name, link: response.link } + ); + } else { + alert(response.error); + } + }, + error: function (response) { + console.log("error", response); + $('.upload-progress[data-field-name=' + field_name + ']').hide(); + } + }); + return false; + }; + + // Trigger Keyboards + editor.commands.addCommand({ + name: 'markdownToBold', + bindKey: { win: 'Ctrl-B', mac: 'Command-B' }, + exec: function (editor) { + markdownToBold(editor); + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'markdownToItalic', + bindKey: { win: 'Ctrl-I', mac: 'Command-I' }, + exec: function (editor) { + markdownToItalic(editor); + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'markdownToUnderscores', + bindKey: { win: 'Ctrl-Shift-U', mac: 'Command-Shift-U' }, + exec: function (editor) { + markdownToUnderscores(editor); + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'markdownToStrikethrough', + bindKey: { win: 'Ctrl-Shift-S', mac: 'Command-Shift-S' }, + exec: function (editor) { + markdownToStrikethrough(editor); + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'markdownToHorizontal', + bindKey: { win: 'Ctrl-H', mac: 'Command-H' }, + exec: function (editor) { + markdownToHorizontal(editor); + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'markdownToH1', + bindKey: { win: 'Ctrl-Alt-1', mac: 'Command-Option-1' }, + exec: function (editor) { + markdownToH1(editor); + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'markdownToH2', + bindKey: { win: 'Ctrl-Alt-2', mac: 'Command-Option-3' }, + exec: function (editor) { + markdownToH2(editor); + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'markdownToH3', + bindKey: { win: 'Ctrl-Alt-3', mac: 'Command-Option-3' }, + exec: function (editor) { + markdownToH3(editor); + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'markdownToPre', + bindKey: { win: 'Ctrl-Alt-P', mac: 'Command-Option-P' }, + exec: function (editor) { + markdownToPre(editor); + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'markdownToCode', + bindKey: { win: 'Ctrl-Alt-C', mac: 'Command-Option-C' }, + exec: function (editor) { + markdownToCode(editor); + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'markdownToBlockQuote', + bindKey: { win: 'Ctrl-Q', mac: 'Command-Shift-K' }, + exec: function (editor) { + markdownToBlockQuote(editor); + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'markdownToUnorderedList', + bindKey: { win: 'Ctrl-U', mac: 'Command-U' }, + exec: function (editor) { + markdownToUnorderedList(editor); + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'markdownToOrderedList', + bindKey: { win: 'Ctrl-Shift+O', mac: 'Command-Option-O' }, + exec: function (editor) { + markdownToOrderedList(editor); + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'markdownToLink', + bindKey: { win: 'Ctrl-L', mac: 'Command-L' }, + exec: function (editor) { + markdownToLink(editor); + }, + readOnly: true + }); + editor.commands.addCommand({ + name: 'markdownToImageLink', + bindKey: { win: 'Ctrl-Shift-I', mac: 'Command-Option-I' }, + exec: function (editor) { + markdownToImageLink(editor); + }, + readOnly: true + }); + if (editorConfig.mention === 'true') { + editor.commands.addCommand({ + name: 'markdownToMention', + bindKey: { win: 'Ctrl-M', mac: 'Command-M' }, + exec: function (editor) { + markdownToMention(editor); + }, + readOnly: true + }); + } + + // Trigger Click + $('.markdown-bold[data-field-name=' + field_name + ']').click(function () { + markdownToBold(editor); + }); + $('.markdown-italic[data-field-name=' + field_name + ']').click(function () { + markdownToItalic(editor); + }); + $('.markdown-horizontal[data-field-name=' + field_name + ']').click(function () { + markdownToHorizontal(editor); + }); + $('.markdown-h1[data-field-name=' + field_name + ']').click(function () { + markdownToH1(editor); + }); + $('.markdown-h2[data-field-name=' + field_name + ']').click(function () { + markdownToH2(editor); + }); + $('.markdown-h3[data-field-name=' + field_name + ']').click(function () { + markdownToH3(editor); + }); + $('.markdown-pre[data-field-name=' + field_name + ']').click(function () { + markdownToPre(editor); + }); + $('.markdown-code[data-field-name=' + field_name + ']').click(function () { + markdownToCode(editor); + }); + $('.markdown-blockquote[data-field-name=' + field_name + ']').click(function () { + markdownToBlockQuote(editor); + }); + $('.markdown-unordered-list[data-field-name=' + field_name + ']').click(function () { + markdownToUnorderedList(editor); + }); + $('.markdown-ordered-list[data-field-name=' + field_name + ']').click(function () { + markdownToOrderedList(editor); + }); + $('.markdown-link[data-field-name=' + field_name + ']').click(function () { + markdownToLink(editor); + }); + $('.markdown-image-link[data-field-name=' + field_name + ']').click(function () { + markdownToImageLink(editor); + }); + + // Custom decission for toolbar buttons. + var btnMention = $('.markdown-direct-mention[data-field-name=' + field_name + ']'); // To Direct Mention + var btnUpload = $('.markdown-image-upload[data-field-name=' + field_name + ']'); // To Upload Image + + if (editorConfig.mention == 'true') { + btnMention.click(function () { + markdownToMention(editor); + }); + } else { + btnMention.remove(); + // Disable help of `mention` + $('.markdown-reference tbody tr')[1].remove(); + } + + if (editorConfig.imgur == 'true') { + btnUpload.on('change', function (evt) { + evt.preventDefault(); + markdownToUploadImage(editor); + }); + } else { + btnUpload.remove(); + } + + // Modal Popup for Help Guide & Emoji Cheat Sheet + $('.markdown-help[data-field-name=' + field_name + ']').click(function () { + $('.modal-help-guide[data-field-name=' + field_name + ']').modal('show'); + }); + + // Toggle editor, preview, maximize + var martorField = $('.martor-field-' + field_name); + var btnToggleMaximize = $('.markdown-toggle-maximize[data-field-name=' + field_name + ']'); + + // Toggle maximize and minimize + var handleToggleMinimize = function () { + $(document.body).removeClass('overflow'); + $(this).attr({ 'title': 'Full Screen' }); + $(this).find('svg.bi-arrows-angle-expand').show(); + $(this).find('svg.bi-arrows-angle-contract').hide(); + $('.main-martor-fullscreen').find('.martor-preview').removeAttr('style'); + mainMartor.removeClass('main-martor-fullscreen'); + martorField.removeAttr('style'); + editor.resize(); + } + var handleToggleMaximize = function (selector) { + selector.attr({ 'title': 'Minimize' }); + selector.find('svg.bi-arrows-angle-expand').hide(); + selector.find('svg.bi-arrows-angle-contract').show(); + mainMartor.addClass('main-martor-fullscreen'); + + var clientHeight = document.body.clientHeight - 90; + // XXX: commented out the field below to prevent weird behaviour + //martorField.attr({ 'style': 'height:' + clientHeight + 'px' }); + + var preview = $('.main-martor-fullscreen').find('.martor-preview'); + preview.attr({ 'style': 'overflow-y: auto;height:' + clientHeight + 'px' }); + + editor.resize(); + selector.one('click', handleToggleMinimize); + $(document.body).addClass('overflow'); + } + btnToggleMaximize.on('click', function () { + handleToggleMaximize($(this)); + }); + + // Exit full screen when `ESC` is pressed. + $(document).keyup(function (e) { + if (e.keyCode == 27 && mainMartor.hasClass('main-martor-fullscreen')) { + btnToggleMaximize.trigger('click'); + } + }); + + // markdown insert emoji from the modal + $('.markdown-emoji[data-field-name=' + field_name + ']').click(function () { + var modalEmoji = $('.modal-emoji[data-field-name=' + field_name + ']').modal('show'); + var emojiList = typeof (emojis) != "undefined" ? emojis : []; // from `plugins/js/emojis.min.js` + var segmentEmoji = modalEmoji.find('.emoji-content-body'); + var loaderInit = modalEmoji.find('.emoji-loader-init'); + + // setup initial loader + segmentEmoji.html(''); + loaderInit.show(); + modalEmoji.show(); + + for (var i = 0; i < emojiList.length; i++) { + var linkEmoji = textareaId.data('base-emoji-url') + emojiList[i].replace(/:/g, '') + '.png'; + segmentEmoji.append('' + + ''); + $('a[data-emoji-target="' + emojiList[i] + '"]').click(function () { + markdownToEmoji(editor, $(this).data('emoji-target')); + modalEmoji.modal('hide'); + }); + } + + loaderInit.hide(); + segmentEmoji.show(); + modalEmoji.modal('handleUpdate'); + }); + + // Set initial value if has the content before. + if (textareaId.val() != '') { + editor.setValue(textareaId.val(), -1); + } + });// end each `mainMartor` + }; + + $(function () { + $('.main-martor').martor(); + }); + + if ('django' in window && 'jQuery' in window.django) + django.jQuery(document).on('formset:added', function (event, $row) { + $row.find('.main-martor').each(function () { + var id = $row.attr('id'); + id = id.substr(id.lastIndexOf('-') + 1); + // Notice here we are using our jQuery instead of Django's. + // This is because plugins are only loaded for ours. + var fixed = $(this.outerHTML.replace(/__prefix__/g, id)); + $(this).replaceWith(fixed); + fixed.martor(); + }); + }); +})(jQuery); From 10cb5281a14f6dc4bf037309b63f2d92bd4990b1 Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Wed, 6 Dec 2023 15:27:32 +0200 Subject: [PATCH 4/8] fix migrations --- ...0_book_content_markdown.py => 0113_book_content_markdown.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename peachjam/migrations/{0110_book_content_markdown.py => 0113_book_content_markdown.py} (84%) diff --git a/peachjam/migrations/0110_book_content_markdown.py b/peachjam/migrations/0113_book_content_markdown.py similarity index 84% rename from peachjam/migrations/0110_book_content_markdown.py rename to peachjam/migrations/0113_book_content_markdown.py index 0cdad35e5..825dcd429 100644 --- a/peachjam/migrations/0110_book_content_markdown.py +++ b/peachjam/migrations/0113_book_content_markdown.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): dependencies = [ - ("peachjam", "0109_add_overturned_and_upheld_predicates"), + ("peachjam", "0112_peachjamsettings_pagerank_pivot_value"), ] operations = [ From 9ac3f4eff7c52ac0399ff9b0db9f69301158d5a9 Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Thu, 7 Dec 2023 11:57:19 +0200 Subject: [PATCH 5/8] some styling --- peachjam/settings.py | 2 +- peachjam/static/martor/css/peachjam.css | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 peachjam/static/martor/css/peachjam.css diff --git a/peachjam/settings.py b/peachjam/settings.py index b5f814d9a..3a2dbbab9 100644 --- a/peachjam/settings.py +++ b/peachjam/settings.py @@ -585,4 +585,4 @@ "hljs": "false", # to enable/disable hljs highlighting in preview } # disable the normal martor theme which pulls in another bootstrap version -MARTOR_ALTERNATIVE_CSS_FILE_THEME = "x" +MARTOR_ALTERNATIVE_CSS_FILE_THEME = "martor/css/peachjam.css" diff --git a/peachjam/static/martor/css/peachjam.css b/peachjam/static/martor/css/peachjam.css new file mode 100644 index 000000000..62927c93a --- /dev/null +++ b/peachjam/static/martor/css/peachjam.css @@ -0,0 +1,4 @@ +.ace_heading { + font-weight: bold; + color: darkgreen; +} From c83b308745e6245278ac4000009c70219aa01ae0 Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Thu, 7 Dec 2023 12:45:39 +0200 Subject: [PATCH 6/8] ensure embeds show up --- peachjam/admin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/peachjam/admin.py b/peachjam/admin.py index e580c684c..cd2fe373b 100644 --- a/peachjam/admin.py +++ b/peachjam/admin.py @@ -988,6 +988,11 @@ class BookAdmin(DocumentAdmin): fieldsets = copy.deepcopy(DocumentAdmin.fieldsets) fieldsets[3][1]["fields"].insert(3, "content_markdown") + class Media: + js = ( + "https://cdn.jsdelivr.net/npm/@lawsafrica/law-widgets@latest/dist/lawwidgets/lawwidgets.js", + ) + def save_model(self, request, obj, form, change): if "content_markdown" in form.changed_data: obj.content_html = markdownify(form.cleaned_data["content_markdown"]) From 48cd456266cc0932cdbd282f2a24ebbdcd149bed Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Thu, 7 Dec 2023 14:51:49 +0200 Subject: [PATCH 7/8] markdown html tables --- .../stylesheets/components/_document-content.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/peachjam/static/stylesheets/components/_document-content.scss b/peachjam/static/stylesheets/components/_document-content.scss index 39efd9534..e7cfe773c 100644 --- a/peachjam/static/stylesheets/components/_document-content.scss +++ b/peachjam/static/stylesheets/components/_document-content.scss @@ -67,6 +67,17 @@ margin-bottom: 0px; } } + + table { + width: 100%; + margin-bottom: 1rem; + border-color: var(--bs-border-color); + + th, td { + border-bottom-width: 1px; + padding: 0.5rem 0.5rem; + } + } } } From c1f164606e07a1265ec917b0fac1f90298023553 Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Mon, 11 Dec 2023 11:29:32 +0200 Subject: [PATCH 8/8] allow superscript and subscript --- peachjam/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/peachjam/settings.py b/peachjam/settings.py index 3a2dbbab9..7d1860d4d 100644 --- a/peachjam/settings.py +++ b/peachjam/settings.py @@ -509,6 +509,8 @@ "Underline", "Strike", "Blockquote", + "Superscript", + "Subscript", "SpellChecker", "Undo", "Redo",