diff --git a/requirements.txt b/requirements.txt index aa34b99d7f..be2fef65a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,6 +36,7 @@ django-ipware==7.0.1 more-itertools==10.1.0 jsmin==3.0.1 kitchen==1.2.4 +latex2mathml==3.78.1 libsass==0.23.0 lxml==6.0.0 markdown diff --git a/src/core/templatetags/latex_mathml.py b/src/core/templatetags/latex_mathml.py new file mode 100644 index 0000000000..c292fcce77 --- /dev/null +++ b/src/core/templatetags/latex_mathml.py @@ -0,0 +1,107 @@ +import re +from xml.etree import ElementTree as ET + +import latex2mathml.converter +import latex2mathml.exceptions + +from django import template + +register = template.Library() + +BLOCK_LATEX_RES = [ + # \[...\] + re.compile(r"\\\[(?P.+?)\\\]"), + # $$...$$ + re.compile(r"\$\$(?P.+?)\$\$"), + # \begin{displaymath}...\end{displaymath} + re.compile(r"\\begin\{displaymath\}(?P.+?)\\end\{displaymath\}"), + # \begin{equation}...\end{equation} + re.compile(r"\\begin\{equation\}(?P.+?)\\end\{equation\}"), +] +INLINE_LATEX_RES = [ + # \(...\) + re.compile(r"\\\((?P.+?)\\\)"), + # $...$ but not $$...$$ + re.compile(r"(?.+?)\$(?!\$)"), + # \begin{math}...\end{math} + re.compile(r"\\begin\{math\}(?P.+?)\\end\{math\}"), +] + +ET.register_namespace("mml", "http://www.w3.org/1998/Math/MathML") + + +def _convert(value, display): + try: + return latex2mathml.converter.convert(value, display=display) + except Exception: + # The input should be returned if there is a parsing error, + # without delimiters to avoid MathJax attempting to parse it. + # We don't want MathJax touching TeX syntax because we support $...$ and + # MathJax does not. + return value + + +def _to_html_block(match): + return _convert(match.group("value"), display="block") + + +def _to_html_inline(match): + return _convert(match.group("value"), display="inline") + + +def _add_mml_namespace_prefix(value): + xml = ET.fromstring(bytes(value, encoding="utf-8")) + return ET.tostring(xml, encoding="unicode") + + +def _to_xml_block(match): + mathml = _convert(match.group("value"), display="block") + return _add_mml_namespace_prefix(mathml) + + +def _to_xml_inline(match): + mathml = _convert(match.group("value"), display="inline") + return _add_mml_namespace_prefix(mathml) + + +def _trim_delimiters(match): + return match.group("value") + + +def _should_parse(journal): + """Whether to parse LaTeX mathematics for this journal""" + if journal: + return journal.get_setting("metadata", "latex_mathematics_title_abstract") + else: + return False + + +@register.filter +def to_mathml(value, journal, target="html", allow_block=False): + assert target in {"xml", "html"} + if _should_parse(journal): + if target == "html": + for pattern in BLOCK_LATEX_RES: + if allow_block: + value = re.sub(pattern, _to_html_block, value) + else: + value = re.sub(pattern, _trim_delimiters, value) + for pattern in INLINE_LATEX_RES: + value = re.sub(pattern, _to_html_inline, value) + elif target == "xml": + for pattern in BLOCK_LATEX_RES: + if allow_block: + value = re.sub(pattern, _to_xml_block, value) + else: + value = re.sub(pattern, _trim_delimiters, value) + for pattern in INLINE_LATEX_RES: + value = re.sub(pattern, _to_xml_inline, value) + return value + + +@register.filter +def strip_latex_delimiters(value, journal): + if _should_parse(journal): + for pattern in BLOCK_LATEX_RES + INLINE_LATEX_RES: + value = re.sub(pattern, _trim_delimiters, value) + return value diff --git a/src/core/tests/test_templatetags.py b/src/core/tests/test_templatetags.py index fb89a72e68..11f8cc96f0 100644 --- a/src/core/tests/test_templatetags.py +++ b/src/core/tests/test_templatetags.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta +from mock import patch import pytz from django.utils import timezone from django.test import TestCase, override_settings @@ -7,7 +8,8 @@ from freezegun import freeze_time from utils.testing import helpers -from core.templatetags import fqdn, dates +from utils import setting_handler +from core.templatetags import fqdn, dates, latex_mathml class TestFqdn(TestCase): @@ -133,3 +135,145 @@ def test_offset_date_with_default_timezone_date(self): input_type="date", ) self.assertEqual(result, expected) + + +class TestLatexMathml(TestCase): + @classmethod + def setUpTestData(cls): + cls.press = helpers.create_press() + cls.journal, _ = helpers.create_journals() + + cls.latex = r""" + one \[x=2\] + two $x=2$ + three $$x=2$$ + four \begin{displaymath}x=2\end{displaymath} + five \begin{equation}x=2\end{equation} + six \(x=2\) + seven \begin{math}x=2\end{math} + """ + setting_handler.save_setting( + "metadata", + "latex_mathematics_title_abstract", + journal=cls.journal, + value="on", + ) + cls.maxDiff = None + + @patch("core.middleware.GlobalRequestMiddleware.get_current_request") + def test_to_mathml_html_allow_block(self, current_request): + current_request.return_value = helpers.Request(journal=self.journal) + expected = r""" + one x=2 + two x=2 + three x=2 + four x=2 + five x=2 + six x=2 + seven x=2 + """ + transformed = latex_mathml.to_mathml( + self.latex, + self.journal, + target="html", + allow_block=True, + ) + self.assertEqual(transformed, expected) + + @patch("core.middleware.GlobalRequestMiddleware.get_current_request") + def test_to_mathml_html_disallow_block(self, current_request): + current_request.return_value = helpers.Request(journal=self.journal) + expected = r""" + one x=2 + two x=2 + three x=2 + four x=2 + five x=2 + six x=2 + seven x=2 + """ + transformed = latex_mathml.to_mathml( + self.latex, + self.journal, + target="html", + allow_block=False, + ) + self.assertEqual(transformed, expected) + + @patch("core.middleware.GlobalRequestMiddleware.get_current_request") + def test_to_mathml_xml(self, current_request): + current_request.return_value = helpers.Request(journal=self.journal) + expected = r""" + one x=2 + two x=2 + three x=2 + four x=2 + five x=2 + six x=2 + seven x=2 + """ + transformed = latex_mathml.to_mathml( + self.latex, + self.journal, + target="xml", + allow_block=True, + ) + self.assertEqual(transformed, expected) + + @patch("core.middleware.GlobalRequestMiddleware.get_current_request") + def test_to_mathml_latex_syntax_error(self, current_request): + current_request.return_value = helpers.Request(journal=self.journal) + value = "some bad latex $${ \over 2}$$" + + # The input should be returned if there is a parsing error, + # without delimiters to avoid MathJax attempting to parse it. + # We don't want MathJax touching TeX syntax because we support $...$ and + # MathJax does not. + expected = "some bad latex { \over 2}" + transformed = latex_mathml.to_mathml(value, self.journal) + self.assertEqual(transformed, expected) + + @patch("core.middleware.GlobalRequestMiddleware.get_current_request") + def test_to_mathml_delimiter_syntax_error(self, current_request): + current_request.return_value = helpers.Request(journal=self.journal) + value = "bad delimiters $$x=2$ should be left alone" + + # The input should be returned as is if there is a parsing error + expected = value + transformed = latex_mathml.to_mathml(value, self.journal) + self.assertEqual(transformed, expected) + + @patch("core.middleware.GlobalRequestMiddleware.get_current_request") + def test_to_mathml_dollars_left_alone(self, current_request): + current_request.return_value = helpers.Request(journal=self.journal) + + # Turn latex parsing off + setting_handler.save_setting( + "metadata", + "latex_mathematics_title_abstract", + journal=self.journal, + value="", + ) + + value = "article about $dollars not math just $dollars" + expected = value + result = latex_mathml.to_mathml(value, self.journal) + self.assertEqual(result, expected) + + @patch("core.middleware.GlobalRequestMiddleware.get_current_request") + def test_strip_delimiters(self, current_request): + current_request.return_value = helpers.Request(journal=self.journal) + expected = r""" + one x=2 + two x=2 + three x=2 + four x=2 + five x=2 + six x=2 + seven x=2 + """ + transformed = latex_mathml.strip_latex_delimiters( + self.latex, + self.journal, + ) + self.assertEqual(transformed, expected) diff --git a/src/identifiers/logic.py b/src/identifiers/logic.py index 33fe0e516b..2606bb3255 100755 --- a/src/identifiers/logic.py +++ b/src/identifiers/logic.py @@ -15,7 +15,7 @@ from django.urls import reverse from django.template.loader import render_to_string from django.utils.http import urlencode -from django.utils.html import strip_tags +from django.utils.html import mark_safe from django.conf import settings from django.contrib import messages from django.utils import timezone @@ -30,6 +30,7 @@ from identifiers import models from submission import models as submission_models + logger = get_logger(__name__) CROSSREF_TIMEOUT_SECONDS = 30 @@ -380,17 +381,13 @@ def create_crossref_journal_context( def create_crossref_article_context(article, identifier=None): template_context = { "id": article.pk, - "title": "{0}{1}{2}".format( - article.title, - " " if article.subtitle is not None else "", - article.subtitle if article.subtitle is not None else "", - ), + "stripped_title": article.stripped_title, "doi": identifier.identifier if identifier else render_doi_from_pattern(article), "url": article.url, "authors": article.frozenauthor_set.all(), - "abstract": strip_tags(article.abstract or ""), + "safe_abstract_jats": article.safe_abstract_jats, "date_accepted": article.date_accepted, "date_published": article.date_published, "license": article.license.url if article.license else "", diff --git a/src/identifiers/tests/test_logic.py b/src/identifiers/tests/test_logic.py index f4ad94be4f..1949603de2 100644 --- a/src/identifiers/tests/test_logic.py +++ b/src/identifiers/tests/test_logic.py @@ -231,8 +231,8 @@ def test_create_crossref_journal_context(self): def test_create_crossref_article_context_published(self): self.maxDiff = None expected_data = { - "title": self.article_published.title, - "abstract": "", + "stripped_title": self.article_published.stripped_title, + "safe_abstract_jats": self.article_published.safe_abstract_jats, "url": self.article_published.url, "authors": [ author.email for author in self.article_published.frozenauthor_set.all() @@ -257,8 +257,8 @@ def test_create_crossref_article_context_published(self): def test_create_crossref_article_context_not_published(self): expected_data = { - "title": self.article_one.title, - "abstract": self.article_one.abstract, + "title": self.article_one.stripped_title, + "abstract": self.article_one.safe_abstract_jats, "url": self.article_one.url, "authors": [ author.email for author in self.article_one.frozenauthor_set.all() diff --git a/src/review/logic.py b/src/review/logic.py index 00574b1084..a9660c6ac7 100755 --- a/src/review/logic.py +++ b/src/review/logic.py @@ -36,6 +36,7 @@ email, models as core_models, ) +from core.templatetags.latex_mathml import to_mathml from review import models from review.const import EditorialDecisions as ED from events import logic as event_logic @@ -234,11 +235,12 @@ def get_article_details_for_review(article): Section: {section}
Keywords: {keywords}
Abstract:
- {article.abstract}
+ {abstract}
""".format( article=article, section=article.section.name if article.section else None, keywords=", ".join(kw.word for kw in article.keywords.all()), + abstract=article.safe_abstract_html, ) return mark_safe(detail_string) diff --git a/src/rss/views.py b/src/rss/views.py index 309e10c47e..83aaa3b015 100755 --- a/src/rss/views.py +++ b/src/rss/views.py @@ -6,7 +6,7 @@ from django.contrib.syndication.views import Feed from django.urls import reverse from django.utils import timezone -from django.template.defaultfilters import striptags +from django.template.defaultfilters import striptags, truncatewords from django.contrib.contenttypes.models import ContentType from comms import models as comms_models @@ -83,10 +83,10 @@ def items(self, obj): ).order_by("-date_published")[:10] def item_title(self, item): - return striptags(item.title) + return item.stripped_title def item_description(self, item): - return truncatesmart(item.abstract, 400) + return truncatewords(item.stripped_abstract, 400) def item_author_name(self, item): if hasattr(item, "posted_by"): diff --git a/src/submission/models.py b/src/submission/models.py index e386db7078..ec359a7855 100755 --- a/src/submission/models.py +++ b/src/submission/models.py @@ -30,6 +30,7 @@ ) from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from django.template.defaultfilters import striptags from django.template import Context, Template from django.template.loader import render_to_string from django.db.models.signals import pre_delete, m2m_changed @@ -51,6 +52,7 @@ ) from core import workflow, model_utils, files, models as core_models from core.templatetags.truncate import truncatesmart +from core.templatetags.latex_mathml import to_mathml, strip_latex_delimiters from identifiers import logic as id_logic from identifiers import models as identifier_models from metrics.logic import ArticleMetrics @@ -1345,13 +1347,90 @@ def authors_and_credits(self): result[frozen_author] = frozen_author.credits return result + @property + def safe_title_html(self): + """Title for use where HTML is supported, marked safe""" + if self.title: + return mark_safe( + to_mathml( + self.title, + self.journal, + target="html", + allow_block=False, + ) + ) + else: + return "[Untitled]" + @property def safe_title(self): + """Use safe_title_html instead""" + return self.safe_title_html + + @property + def safe_title_jats(self): + """Title for use in JATS XML, marked safe""" if self.title: - return mark_safe(self.title) + return mark_safe( + to_mathml( + self.title, + self.journal, + target="xml", + allow_block=False, + ) + ) else: return "[Untitled]" + @property + def stripped_title(self): + """ + The title without HTML or XML tags or LaTeX delimiters. + """ + if self.title: + return striptags( + strip_latex_delimiters( + self.title, + self.journal, + ) + ) + else: + return "[Untitled]" + + @property + def safe_abstract_html(self): + """Abstract with HTML, marked safe""" + return mark_safe( + to_mathml( + self.abstract, + self.journal, + target="html", + allow_block=True, + ) + ) + + @property + def safe_abstract_jats(self): + """Abstract for use in JATS XML, marked safe""" + if not self.abstract: + return "" + return transform_utils.convert_html_abstract_to_jats( + self.abstract, + self.journal, + ) + + @property + def stripped_abstract(self): + """ + The abstract without HTML tags or LaTeX delimiters. + """ + return striptags( + strip_latex_delimiters( + self.abstract, + self.journal, + ) + ) + @property def how_to_cite(self): if self.custom_how_to_cite: @@ -2533,11 +2612,10 @@ def pinned(self): def get_clean_abstract(self): """ - Returns a JATS-safe abstract with only allowed inline tags and wrapped in

. + Returns a JATS-safe abstract with a chosen subset of tags. """ - if not self.abstract: - return "" - return transform_utils.convert_html_abstract_to_jats(self.abstract) + warnings.warn("Deprecated. Use safe_abstract_jats.") + return self.safe_abstract_jats @property def iso639_1_lang_code(self): diff --git a/src/templates/admin/elements/metadata.html b/src/templates/admin/elements/metadata.html index 5fdaff1d52..1c1be61712 100644 --- a/src/templates/admin/elements/metadata.html +++ b/src/templates/admin/elements/metadata.html @@ -46,7 +46,7 @@

{{ article.safe_title }}

Abstract - {{ article.abstract|safe }} + {{ article.safe_abstract_html }} Keywords diff --git a/src/templates/admin/review/share/editor.html b/src/templates/admin/review/share/editor.html index 0e75201594..4d15df78ec 100644 --- a/src/templates/admin/review/share/editor.html +++ b/src/templates/admin/review/share/editor.html @@ -5,7 +5,7 @@ {% block title-section %}Sharing Peer Reviews{% endblock %} {% block title-sub %}#{{ article.pk }} / {{ article.correspondence_author.last_name }} / - {{ article.title }}{% endblock %} + {{ article.safe_title_html }}{% endblock %} {% block breadcrumbs %} {{ block.super }} diff --git a/src/templates/admin/review/share/reviewer.html b/src/templates/admin/review/share/reviewer.html index ae8d475d13..82e7a91764 100644 --- a/src/templates/admin/review/share/reviewer.html +++ b/src/templates/admin/review/share/reviewer.html @@ -18,7 +18,9 @@

About Shared Reviews

-

The following reviews have been shared with you were written for "{{ article.title|safe }}". They have also been shared with the other reviews of this article.

+

The following reviews have been shared with you were + written for "{{ article.safe_title_html }}". + They have also been shared with the other reviews of this article.

diff --git a/src/templates/admin/review/unassigned_article.html b/src/templates/admin/review/unassigned_article.html index 0ff9802808..803b992dbd 100644 --- a/src/templates/admin/review/unassigned_article.html +++ b/src/templates/admin/review/unassigned_article.html @@ -1,7 +1,7 @@ {% extends "admin/core/base.html" %} {% load static roles i18n securitytags %} -{% block title %}Unassigned {{ article.title }}{% endblock %} +{% block title %}Unassigned {{ article.stripped_title }}{% endblock %} {% block title-section %}Unassigned{% endblock %} {% block title-sub %}#{{ article.pk }} / {{ article.correspondence_author.last_name|se_can_see_pii:article }} / {{ article.safe_title }}{% endblock %} @@ -46,7 +46,7 @@

Summary of Article

Abstract - {{ article.abstract|safe }} + {{ article.safe_abstract_html }} {% if journal_settings.general.submission_summary %} diff --git a/src/templates/common/apis/OAI_record.xml b/src/templates/common/apis/OAI_record.xml index 746c00f87b..771a473b8d 100644 --- a/src/templates/common/apis/OAI_record.xml +++ b/src/templates/common/apis/OAI_record.xml @@ -12,7 +12,7 @@ {% for author in article.frozenauthor_set.all %} {{ author.dc_name_string }} {% endfor %} - {{article.abstract|striptags}} + {{article.stripped_abstract}} {{article.date_published|date:"Y-m-d\TH:i:s\Z"}} {% if journal.is_conference %} info:eu-repo/semantics/conferenceObject diff --git a/src/templates/common/elements/article_meta_tags.html b/src/templates/common/elements/article_meta_tags.html index 3d264caa1d..9691a77b51 100644 --- a/src/templates/common/elements/article_meta_tags.html +++ b/src/templates/common/elements/article_meta_tags.html @@ -12,7 +12,7 @@ {% if article.date_published %}{% endif %} - + diff --git a/src/templates/common/elements/journal/article_title.html b/src/templates/common/elements/journal/article_title.html index f0f3abe908..140d5600f2 100644 --- a/src/templates/common/elements/journal/article_title.html +++ b/src/templates/common/elements/journal/article_title.html @@ -1,2 +1,2 @@ -{{ article.title|striptags }} | -{{ journal_settings.general.journal_name|striptags }} \ No newline at end of file +{{ article.stripped_title }} | +{{ journal_settings.general.journal_name|striptags }} diff --git a/src/templates/common/elements/journal/social_meta.html b/src/templates/common/elements/journal/social_meta.html index a062077ba7..0ac32f2c65 100644 --- a/src/templates/common/elements/journal/social_meta.html +++ b/src/templates/common/elements/journal/social_meta.html @@ -1,7 +1,9 @@ +{% load latex_mathml %} + {% if journal_settings.general.twitter_handle %}{% endif %} - + {% if article.correspondence_author.twitter_handle %}{% endif %} @@ -9,5 +11,5 @@ - + diff --git a/src/templates/common/encoding/article_jats_1_2.xml b/src/templates/common/encoding/article_jats_1_2.xml index 34d05453fc..7005c83675 100644 --- a/src/templates/common/encoding/article_jats_1_2.xml +++ b/src/templates/common/encoding/article_jats_1_2.xml @@ -33,7 +33,7 @@ - {{ article.title }} + {{ article.safe_title_jats }} {% for item in article.frozen_authors_for_jats_contribs %} @@ -150,7 +150,7 @@ {% if article.pdfs.exists %} {% endif %} - {{ article.get_clean_abstract }} + {{ article.safe_abstract_jats }} {% if article.keywords.exists %} Keywords diff --git a/src/templates/common/identifiers/crossref_article.xml b/src/templates/common/identifiers/crossref_article.xml index 9c4b073264..bacb74290d 100755 --- a/src/templates/common/identifiers/crossref_article.xml +++ b/src/templates/common/identifiers/crossref_article.xml @@ -2,7 +2,7 @@ {% if article.scheduled %} - {{ article.title }} + {{ article.stripped_title }} {% else %} Title Pending {{ article.pk }} {% endif %} @@ -10,9 +10,9 @@ {% include "common/identifiers/crossref_contributors.xml" %} - {% if article.abstract and article.scheduled %} + {% if article.abstract and article.scheduled %} - {{ article.get_clean_abstract }} + {{ article.safe_abstract_jats }} {% endif %} diff --git a/src/templates/common/identifiers/crossref_doi_batch.xml b/src/templates/common/identifiers/crossref_doi_batch.xml index 7706c771ed..d526e96826 100644 --- a/src/templates/common/identifiers/crossref_doi_batch.xml +++ b/src/templates/common/identifiers/crossref_doi_batch.xml @@ -1,6 +1,7 @@ diff --git a/src/themes/OLH/templates/elements/journal/box_article.html b/src/themes/OLH/templates/elements/journal/box_article.html index 02aa7331a9..a3f9ba7035 100644 --- a/src/themes/OLH/templates/elements/journal/box_article.html +++ b/src/themes/OLH/templates/elements/journal/box_article.html @@ -22,7 +22,7 @@ {% trans 'Pinned' %} {% endif %} -

{{ article.title|safe }} +

{{ article.safe_title_html }} {% if article.is_remote %}   , {% trans 'externally hosted article' %}. diff --git a/src/themes/OLH/templates/elements/journal/citation_modals.html b/src/themes/OLH/templates/elements/journal/citation_modals.html index 6219044f3e..d4e91b1189 100644 --- a/src/themes/OLH/templates/elements/journal/citation_modals.html +++ b/src/themes/OLH/templates/elements/journal/citation_modals.html @@ -5,7 +5,7 @@

{% trans 'Harvard-style Citation' %}

{% for author in article.frozenauthor_set.all %}{% if not forloop.first and not forloop.last %}, {% elif forloop.last and not forloop.first %}& {% endif %}{{ author.last_name }}{% if author.name_suffix %} {{ author.name_suffix }}{% endif %}, {{ author.first_name|slice:"1" }}{% if forloop.last %}.{% endif %} {% endfor %} - ({{ article.date_published.year }}) '{{ article.title|safe }}', + ({{ article.date_published.year }}) '{{ article.safe_title_html }}', {% if journal.name %}{{ journal.name }}{% else %}{{ request.press.name }} {% trans 'Preprints' %}{% endif %}. {% if article.issue and article.issue.issue and article.issue.volume %} {{ article.issue.volume }}({{ article.issue.issue }}) @@ -27,7 +27,7 @@

{% trans 'Harvard-style Citation' %}

{% trans 'Vancouver-style Citation' %}

{% for author in article.frozenauthor_set.all %}{% if not forloop.first and not forloop.last %}, {% elif forloop.last and not forloop.first %}& {% endif %}{{ author.last_name }}{% if author.name_suffix %} {{ author.name_suffix }}{% endif %}, {{ author.first_name|slice:"1" }}{% if forloop.last %}.{% endif %} {% endfor %} - {{ article.title|safe }}. {% if journal.name %}{{ journal.name }}{% else %}{{ request.press.name }} {% trans 'Preprints' %}{% endif %}. {{ article.date_published.year }} {{ article.date_published.month }}; + {{ article.safe_title_html }}. {% if journal.name %}{{ journal.name }}{% else %}{{ request.press.name }} {% trans 'Preprints' %}{% endif %}. {{ article.date_published.year }} {{ article.date_published.month }}; {% if article.issue and article.issue.issue and article.issue.volume %} {{ article.issue.volume }}({{ article.issue.issue }}) {% elif article.issue and article.issue.issue %} @@ -50,7 +50,7 @@

{% trans 'APA-style Citation' %}

{% for author in article.frozenauthor_set.all %}{% if forloop.last %}{% if not forloop.first %} & {% endif %}{% endif %}{{ author.last_name }}, {{ author.first_name|slice:"1" }}{% if forloop.last %}.{% endif %} {% endfor %} - ({{ article.date_published.year }}). {{ article.title|safe }}. + ({{ article.date_published.year }}). {{ article.safe_title_html }}. {% spaceless %} {% if journal.name %}{{ journal.name }}{% else %}{{ request.press.name }} {% trans 'Preprints' %}{% endif %} {% if article.issue and article.issue.issue and article.issue.volume %} diff --git a/src/themes/OLH/templates/elements/journal/how_to_cite.html b/src/themes/OLH/templates/elements/journal/how_to_cite.html index b3b6e82d57..36f6597a09 100644 --- a/src/themes/OLH/templates/elements/journal/how_to_cite.html +++ b/src/themes/OLH/templates/elements/journal/how_to_cite.html @@ -1,5 +1,5 @@ {% for author in article.frozen_authors.all %}{% if not forloop.first and not forloop.last %}, {% elif forloop.last and not forloop.first %}& {% endif %}{{ author.citation_name }} {% endfor %} -({{ article.date_published.year }}) “{{ article.title|safe }}”, +({{ article.date_published.year }}) “{{ article.safe_title_html }}”, {{ journal.name }}.{% if article.issue.volume %} {{ article.issue.volume }}{% endif %}{% if article.issue.issue %}({{ article.issue.issue }}).{% endif %} -{% if article.identifier.id_type == 'doi' %}doi: https://doi.org/{{ article.identifier.identifier }}{% endif %}

\ No newline at end of file +{% if article.identifier.id_type == 'doi' %}doi: https://doi.org/{{ article.identifier.identifier }}{% endif %}

diff --git a/src/themes/OLH/templates/journal/article.html b/src/themes/OLH/templates/journal/article.html index 4bb98e6814..66252811b2 100644 --- a/src/themes/OLH/templates/journal/article.html +++ b/src/themes/OLH/templates/journal/article.html @@ -22,7 +22,7 @@
{{ article.section.name }} -

{{ article.title|safe }}

+

{{ article.safe_title_html }}

{% blocktrans count counter=article.frozen_authors.count %} Author @@ -52,7 +52,7 @@

{{ article.title|safe }}

{{ article.section.name }}

-

{{ article.title|safe }}

+

{{ article.safe_title_html }}

{% blocktrans count counter=article.frozen_authors.count %} Author @@ -129,7 +129,7 @@

{{ article.title|safe }}

{% if journal_settings.article.disable_article_large_image %} {{ article.section.name }} -

{{ article.title|safe }}

+

{{ article.safe_title_html }}

{% blocktrans count counter=article.frozen_authors.count %} @@ -144,7 +144,7 @@

{{ article.title|safe }}

{% endif %} {% if article.abstract and article.abstract != ''%}

{% trans "Abstract" %}

-

{{ article.abstract|safe }}

+

{{ article.safe_abstract_html }}

{% endif %} {% if article.keywords.count > 0 %}

{% trans "Keywords" %}: {% for keyword in article.keywords.all %}{% if journal_settings.general.keyword_list_page %}{% endif %}{{ keyword.word }}{% if journal_settings.general.keyword_list_page %}{% endif %}{% if not forloop.last %}, {% endif %}{% endfor %}

{% endif %} {% if article.is_published or proofing %} diff --git a/src/themes/OLH/templates/journal/print.html b/src/themes/OLH/templates/journal/print.html index 415c1d390c..60af381d82 100644 --- a/src/themes/OLH/templates/journal/print.html +++ b/src/themes/OLH/templates/journal/print.html @@ -1,6 +1,7 @@ {% load static %} {% load hooks %} {% load i18n %} + @@ -11,7 +12,7 @@

{{ article.section.name }}

-

{{ article.title|safe }}

+

{{ article.safe_title_html }}

{% blocktrans count counter=article.frozen_authors.count %} @@ -23,7 +24,7 @@

{{ article.title|safe }}

{% include "common/elements/journal/article_authors_full.html" %}

{% trans "Abstract" %}

-

{{ article.abstract | safe }}

+

{{ article.safe_abstract_html|safe }}

{% if article.keywords %}

{% trans "Keywords" %}: {% for keyword in article.keywords.all %}{{ keyword.word }}{% if not forloop.last %}, {% endif %}{% endfor %}

{% endif %} diff --git a/src/themes/clean/templates/elements/article_listing.html b/src/themes/clean/templates/elements/article_listing.html index ed80c832f5..b9bc6956f4 100644 --- a/src/themes/clean/templates/elements/article_listing.html +++ b/src/themes/clean/templates/elements/article_listing.html @@ -23,11 +23,11 @@ {% if article.is_remote %}

{% trans 'Externally hosted article' %}: - {{ article.title|safe }}  + {{ article.safe_title_html }} 

{% else %} -

{{ article.title|safe }}

+

{{ article.safe_title_html }}

{% endif %}

{% for author in article.frozen_authors.all %}{% if forloop.last %} diff --git a/src/themes/clean/templates/elements/journal/citation_modals.html b/src/themes/clean/templates/elements/journal/citation_modals.html index 25a8d36312..78d37fa492 100644 --- a/src/themes/clean/templates/elements/journal/citation_modals.html +++ b/src/themes/clean/templates/elements/journal/citation_modals.html @@ -12,7 +12,7 @@

@@ -34,7 +34,7 @@
@@ -55,7 +55,7 @@

{% for author in article.frozenauthor_set.all %}{% if forloop.last %}{% if not forloop.first %} & {% endif %}{% endif %}{{ author.last_name }}, {{ author.first_name|slice:"1" }}{% if forloop.last %}.{% endif %} {% endfor %} - ({{ article.date_published.year }}, {{ article.date_published.month }} {{ article.date_published.day }}). {{ article.title|safe }}. + ({{ article.date_published.year }}, {{ article.date_published.month }} {{ article.date_published.day }}). {{ article.safe_title_html }}. {% if journal.name %}{{ journal.name }}{% else %}{{ request.press.name }} Preprints{% endif %} {% if article.issue %}{{ article.issue.volume }}({{ article.issue.issue }}){% endif %}{% if article.page_range %}:{{ article.page_range }}.{% endif %} {% if article.identifier.id_type == 'doi' %}doi: {{ article.identifier.identifier }}{% endif %}

diff --git a/src/themes/clean/templates/elements/journal/how_to_cite.html b/src/themes/clean/templates/elements/journal/how_to_cite.html index b3b6e82d57..36f6597a09 100644 --- a/src/themes/clean/templates/elements/journal/how_to_cite.html +++ b/src/themes/clean/templates/elements/journal/how_to_cite.html @@ -1,5 +1,5 @@ {% for author in article.frozen_authors.all %}{% if not forloop.first and not forloop.last %}, {% elif forloop.last and not forloop.first %}& {% endif %}{{ author.citation_name }} {% endfor %} -({{ article.date_published.year }}) “{{ article.title|safe }}”, +({{ article.date_published.year }}) “{{ article.safe_title_html }}”, {{ journal.name }}.{% if article.issue.volume %} {{ article.issue.volume }}{% endif %}{% if article.issue.issue %}({{ article.issue.issue }}).{% endif %} -{% if article.identifier.id_type == 'doi' %}doi: https://doi.org/{{ article.identifier.identifier }}{% endif %}

\ No newline at end of file +{% if article.identifier.id_type == 'doi' %}doi: https://doi.org/{{ article.identifier.identifier }}{% endif %}

diff --git a/src/themes/clean/templates/journal/article.html b/src/themes/clean/templates/journal/article.html index 35e8bcc7ec..a4ca9f774d 100644 --- a/src/themes/clean/templates/journal/article.html +++ b/src/themes/clean/templates/journal/article.html @@ -32,7 +32,9 @@

{{ article.section.name }}

-

{{ article.title|safe }}

+

+ {{ article.safe_title_html }} +

{% include "elements/journal/article_authors_compact.html" with limit=10 %}
@@ -46,11 +48,11 @@

{{ article.title|safe }}<
{% if journal_settings.article.disable_article_large_image %} {{ article.section.name }} -

{{ article.title|safe }}

+

{{ article.safe_title_html }}

{% endif %} {% if article.abstract and article.abstract != '' %}

{% trans "Abstract" %}

-

{{ article.abstract | safe }}

+

{{ article.safe_abstract_html }}

{% endif %} {% if article.keywords and article.keywords.count > 0 %}

{% trans "Keywords" %}:

diff --git a/src/themes/clean/templates/journal/print.html b/src/themes/clean/templates/journal/print.html index 8fbe93a76e..9c2060f4d3 100644 --- a/src/themes/clean/templates/journal/print.html +++ b/src/themes/clean/templates/journal/print.html @@ -1,6 +1,7 @@ {% load static %} {% load hooks %} {% load i18n %} + @@ -10,7 +11,7 @@ </head> <body> <p class="uppercase">{{ article.section.name }}</p> -<h3>{{ article.title|safe }}</h3> +<h3>{{ article.safe_title_html }}</h3> <p> <strong> {% blocktrans count counter=article.frozen_authors.count %} @@ -23,7 +24,7 @@ <h3>{{ article.title|safe }}</h3> {% include "common/elements/journal/article_authors_full.html" %} <h2>{% trans "Abstract" %}</h2> -<p>{{ article.abstract | safe }}</p> +<p>{{ article.safe_abstract_html }}</p> {% if article.keywords %} <p><strong>{% trans "Keywords" %}:</strong> {% for keyword in article.keywords.all %}{{ keyword.word }}{% if not forloop.last %}, {% endif %}{% endfor %} </p>{% endif %} diff --git a/src/themes/material/templates/elements/article_listing.html b/src/themes/material/templates/elements/article_listing.html index ccd6ca08c3..329e482abb 100644 --- a/src/themes/material/templates/elements/article_listing.html +++ b/src/themes/material/templates/elements/article_listing.html @@ -19,7 +19,7 @@ {% endif %} <div class="col m{% if not journal_settings.article.disable_article_thumbnails %}10{% else %}12{% endif %} s12"> <a href="{% if article.is_remote %}{{ article.remote_url }}{% else %}{{ article.url }}{% endif %}"> - <h5 class="article-title">{{ article.title|safe }}</h5> + <h5 class="article-title">{{ article.safe_title_html }}</h5> </a> <p>{% for author in article.frozen_authors.all %}{% if forloop.last %} {% if article.frozen_authors.all|length > 1 %} {% trans "and" %} diff --git a/src/themes/material/templates/journal/article.html b/src/themes/material/templates/journal/article.html index f0c4f24d0f..5e82693a82 100644 --- a/src/themes/material/templates/journal/article.html +++ b/src/themes/material/templates/journal/article.html @@ -40,7 +40,7 @@ {{ article.section.name }} </small> <br/> - {{ article.title|safe }} + {{ article.safe_title_html }} </div> </span> <span class="card-title" style="font-size: 17px;"> @@ -49,7 +49,7 @@ {{ article.section.name }} <br/> - {{ article.title|safe }}</small> + {{ article.safe_title_html }}</small> </div> </span> </div> @@ -66,13 +66,13 @@ {% if journal_settings.article.disable_article_large_image %} <span class="card-title"> <small class="article_section">{{ article.section.name }}</small> - <h1>{{ article.title|safe }}</h1> + <h1>{{ article.safe_title_html }}</h1> </span> {% endif %} {% if article.abstract and article.abstract != '' %} <h2>{% trans "Abstract" %}</h2> - <p>{{ article.abstract | safe }}</p> + <p>{{ article.safe_abstract_html|safe }}</p> <div class="spacer"> <div class="divider"></div> </div> diff --git a/src/transform/utils.py b/src/transform/utils.py index 04819980d5..a8d7bd02eb 100644 --- a/src/transform/utils.py +++ b/src/transform/utils.py @@ -5,12 +5,13 @@ from django.conf import settings from django.utils.safestring import mark_safe +from core.templatetags.latex_mathml import to_mathml from utils.logger import get_logger logger = get_logger(__name__) -def convert_html_abstract_to_jats(abstract_string): +def convert_html_abstract_to_jats(abstract_string, journal=None): if not abstract_string: return "" @@ -39,7 +40,8 @@ def convert_html_abstract_to_jats(abstract_string): xml_str = xml_str.replace(' xmlns:xlink="http://www.w3.org/1999/xlink"', "") xml_str = xml_str.replace("<root>", "").replace("</root>", "").strip() - return mark_safe(xml_str) + mathml = to_mathml(xml_str, journal, target="xml", allow_block=True) + return mark_safe(mathml) except Exception as e: logger.error(e) diff --git a/src/typesetting/templates/typesetting/breadcrumbs/typesetting_base.html b/src/typesetting/templates/typesetting/breadcrumbs/typesetting_base.html index 3a738a0212..e0b1a458e9 100644 --- a/src/typesetting/templates/typesetting/breadcrumbs/typesetting_base.html +++ b/src/typesetting/templates/typesetting/breadcrumbs/typesetting_base.html @@ -4,6 +4,8 @@ {% if article %} <li> - <a href="{% url 'typesetting_article' article.pk %}">{{ article.title|safe }}</a> + <a href="{% url 'typesetting_article' article.pk %}"> + {{ article.safe_title_html }} + </a> </li> -{% endif %} \ No newline at end of file +{% endif %} diff --git a/src/typesetting/templates/typesetting/elements/typesetter/metadata.html b/src/typesetting/templates/typesetting/elements/typesetter/metadata.html index da9e06e875..98c75783c0 100644 --- a/src/typesetting/templates/typesetting/elements/typesetter/metadata.html +++ b/src/typesetting/templates/typesetting/elements/typesetter/metadata.html @@ -36,7 +36,7 @@ <th colspan="4">Abstract</th> </tr> <tr> - <td colspan="4">{{ article.abstract|safe }}</td> + <td colspan="4">{{ article.safe_abstract_html }}</td> </tr> <tr> <th colspan="4">Keywords</th> diff --git a/src/typesetting/templates/typesetting/typesetting_assign_proofreader.html b/src/typesetting/templates/typesetting/typesetting_assign_proofreader.html index a53b30edb8..aa22170279 100644 --- a/src/typesetting/templates/typesetting/typesetting_assign_proofreader.html +++ b/src/typesetting/templates/typesetting/typesetting_assign_proofreader.html @@ -16,7 +16,7 @@ {% block body %} <div class="box"> <div class="title-area"> - <h2>Assign Proofreader to {{ article.title|safe }}</h2> + <h2>Assign Proofreader to {{ article.safe_title_html }}</h2> <a class="button" href="{% url 'core_manager_role' 'proofreader' %}" target="_blank">Enrol a Proofreader</a> </div> <div class="content"> diff --git a/src/typesetting/templates/typesetting/typesetting_assignment.html b/src/typesetting/templates/typesetting/typesetting_assignment.html index c1c2f4f803..cc089f25c6 100644 --- a/src/typesetting/templates/typesetting/typesetting_assignment.html +++ b/src/typesetting/templates/typesetting/typesetting_assignment.html @@ -11,7 +11,7 @@ {% block breadcrumbs %} <li><a href="{% url 'typesetting_assignments' %}">Typesetting Assignments</a></li> - <li>Typesetting {{ assignment.round.article.title|safe }}</li> + <li>Typesetting {{ assignment.round.article.safe_title_html }}</li> {% endblock breadcrumbs %} {% block body %} @@ -24,7 +24,7 @@ <h2>Assignment Information</h2> </div> <p> - {{ article.safe_title }} [<a href="#view-metadata">Skip to full metadata</a>] + {{ article.safe_title_html }} [<a href="#view-metadata">Skip to full metadata</a>] </p> <div class="content"> <p><strong>Typesetting Guide</strong></p> diff --git a/src/typesetting/templates/typesetting/typesetting_assignments.html b/src/typesetting/templates/typesetting/typesetting_assignments.html index e60ff46a55..dffa3217ff 100644 --- a/src/typesetting/templates/typesetting/typesetting_assignments.html +++ b/src/typesetting/templates/typesetting/typesetting_assignments.html @@ -27,7 +27,7 @@ <h2>Your Typesetting Assignments</h2> {% for assignment in active_assignments %} <tbody> <td>{{ assignment.pk }}</td> - <td>{{ assignment.round.article.title|safe }}</td> + <td>{{ assignment.round.article.safe_title_html }}</td> <td>{{ assignment.round }}</td> <td>{{ assignment.assigned }}</td> <td>{{ assignment.due|date:"Y-m-d" }}</td> @@ -55,7 +55,7 @@ <h2>Past Typesetting Assignments</h2> {% for assignment in past_assignments %} <tbody> <td>{{ assignment.pk }}</td> - <td>{{ assignment.round.article.title|safe }}</td> + <td>{{ assignment.round.article.safe_title_html }}</td> <td>{{ assignment.round }}</td> <td>{{ assignment.assigned }}</td> <td>{% if assignment.completed %}Completed: {{ assignment.completed }}{% else %}Cancelled: {{ assignment.cancelled }}{% endif %}</td> diff --git a/src/typesetting/templates/typesetting/typesetting_proofing_assignments.html b/src/typesetting/templates/typesetting/typesetting_proofing_assignments.html index 7379ca4ba1..22820eaa2f 100644 --- a/src/typesetting/templates/typesetting/typesetting_proofing_assignments.html +++ b/src/typesetting/templates/typesetting/typesetting_proofing_assignments.html @@ -27,7 +27,7 @@ <h2>Your Open Proofreading Assignments</h2> {% for assignment in active_assignments %} <tbody> <td>{{ assignment.pk }}</td> - <td>{{ assignment.round.article.title|safe }}</td> + <td>{{ assignment.round.article.safe_title_html }}</td> <td>{{ assignment.assigned }}</td> <td>{{ assignment.due|date:"Y-m-d" }}</td> <td>{{ assignment.time_to_due }}</td> @@ -67,4 +67,4 @@ <h2>Your Closed Proofreading Assignments</h2> {% block js %} {% include "elements/datatables.html" with target="#tasks" %} {% include "elements/datatables.html" with target="#completed_tasks" %} -{% endblock js %} \ No newline at end of file +{% endblock js %} diff --git a/src/typesetting/templates/typesetting/typesetting_proofreading_assignment.html b/src/typesetting/templates/typesetting/typesetting_proofreading_assignment.html index a827c34a4b..dda26b48dd 100644 --- a/src/typesetting/templates/typesetting/typesetting_proofreading_assignment.html +++ b/src/typesetting/templates/typesetting/typesetting_proofreading_assignment.html @@ -7,14 +7,14 @@ {% block breadcrumbs %} {{ block.super }} <li><a href="{% url 'typesetting_proofreading_assignments' %}">Proofreading Assignments</a></li> - <li>Proofreading {{ assignment.round.article.title|safe }}</li> + <li>Proofreading {{ assignment.round.article.safe_title_html }}</li> {% endblock breadcrumbs %} {% block body %} <div class="large-12 columns"> <div class="box"> <div class="title-area"> - <h2>Proofreading {{ assignment.round.article.title|safe }}</h2> + <h2>Proofreading {{ assignment.round.article.safe_title_html }}</h2> </div> <div class="content"> {{ journal_settings.general.typesetting_proofreader_guidelines|safe }} diff --git a/src/utils/install/journal_defaults.json b/src/utils/install/journal_defaults.json index ce2c9742e9..d36eb62f9b 100644 --- a/src/utils/install/journal_defaults.json +++ b/src/utils/install/journal_defaults.json @@ -4908,7 +4908,7 @@ "type": "rich-text" }, "value": { - "default": "<p>An email sent to {% for email in event.to %}{{ email }}{% if not forloop.last %}, {% endif %}{% endfor %} has not been delivered.{% if target %} This message was in regards to {{ event.content_type.model }} #{{ target.pk }} \"{{ target.title|safe }}\".{% endif %}</p><p>Kind Regards,</p>" + "default": "<p>An email sent to {% for email in event.to %}{{ email }}{% if not forloop.last %}, {% endif %}{% endfor %} has not been delivered.{% if target %} This message was in regards to {{ event.content_type.model }} #{{ target.pk }} \"{{ target.safe_title }}\".{% endif %}</p><p>Kind Regards,</p>" }, "editable_by": [ "journal-manager", @@ -5175,7 +5175,7 @@ "type": "rich-text" }, "value": { - "default": "<p>Dear {{ assignment.manager.full_name }},</p><p>This is to notify you that {{ assignment.typesetter.full_name }} has accepted your assignment your assignment for {{ assignment.round.article.title|safe }}.</p>{% if note %}<p>They provided the following note: <br />{{ note|safe }} </p>{% endif %}<p>Regards,</p>" + "default": "<p>Dear {{ assignment.manager.full_name }},</p><p>This is to notify you that {{ assignment.typesetter.full_name }} has accepted your assignment your assignment for {{ assignment.round.article.safe_title }}.</p>{% if note %}<p>They provided the following note: <br />{{ note|safe }} </p>{% endif %}<p>Regards,</p>" } }, { @@ -5205,7 +5205,7 @@ "type": "rich-text" }, "value": { - "default": "<p>Dear {{ assignment.manager.full_name }},</p><p>This is to notify you that {{ assignment.typesetter.full_name }} has declined your assignment for {{ assignment.round.article.title|safe }}.</p>{% if note %}<p>They provided the following note: <br />{{ note|safe }} </p>{% endif %}<p>Regards,</p>" + "default": "<p>Dear {{ assignment.manager.full_name }},</p><p>This is to notify you that {{ assignment.typesetter.full_name }} has declined your assignment for {{ assignment.round.article.safe_title }}.</p>{% if note %}<p>They provided the following note: <br />{{ note|safe }} </p>{% endif %}<p>Regards,</p>" } }, { @@ -5235,7 +5235,7 @@ "type": "rich-text" }, "value": { - "default": "<p>Dear {{ assignment.manager.full_name }},</p><p>This is to notify you that {{ assignment.typesetter.full_name }} has completed your assignment for {{ assignment.round.article.title|safe }}.</p>{% if note %}<p>They provided the following note: <br />{{ assignment.typesetter_note|safe }} </p>{% endif %}<p>Visit the article URL for more details: {{ typesetting_article_url }}</p><p>Regards,</p>" + "default": "<p>Dear {{ assignment.manager.full_name }},</p><p>This is to notify you that {{ assignment.typesetter.full_name }} has completed your assignment for {{ assignment.round.article.safe_title }}.</p>{% if note %}<p>They provided the following note: <br />{{ assignment.typesetter_note|safe }} </p>{% endif %}<p>Visit the article URL for more details: {{ typesetting_article_url }}</p><p>Regards,</p>" } }, { @@ -5653,5 +5653,24 @@ "editor", "journal-manager" ] + }, + { + "group": { + "name": "metadata" + }, + "setting": { + "description": "Parse LaTeX mathematics in titles and abstracts?", + "is_translatable": false, + "name": "latex_mathematics_title_abstract", + "pretty_name": "LaTeX mathematics in title and abstract", + "type": "boolean" + }, + "value": { + "default": "" + }, + "editable_by": [ + "editor", + "journal-manager" + ] } ]