From 2bdec137af7d0dca2cd7e09ce4b09ceac09bffb9 Mon Sep 17 00:00:00 2001
From: Michael Vasseur <14887731+vmcj@users.noreply.github.com>
Date: Tue, 28 Oct 2025 21:43:44 +0100
Subject: [PATCH 1/3] Get MathJax from yarn
---
webapp/package.json | 1 +
webapp/yarn.lock | 12 ++++++++++++
2 files changed, 13 insertions(+)
diff --git a/webapp/package.json b/webapp/package.json
index c97d546996..4219d31cbb 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -11,6 +11,7 @@
"file-saver": "^2.0.5",
"flag-icons": "^7.5.0",
"jquery-debounce-throttle": "^1.0.6-rc.0",
+ "mathjax": "^4.0.0",
"monaco-editor": "^0.54.0",
"nvd3": "1.8.6",
"select2": "^4.0.13",
diff --git a/webapp/yarn.lock b/webapp/yarn.lock
index e2e9f4c3c0..0c7d920b65 100644
--- a/webapp/yarn.lock
+++ b/webapp/yarn.lock
@@ -7,6 +7,11 @@
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-7.1.0.tgz#8eb76278515341720aa74485266f8be121089529"
integrity sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA==
+"@mathjax/mathjax-newcm-font@^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/@mathjax/mathjax-newcm-font/-/mathjax-newcm-font-4.0.0.tgz#01afa8b20f2114665dc1d9a7fbc623a23ddb54e4"
+ integrity sha512-kpsJgIF4FpWiwIkFgOPmWwy5GXfL25spmJJNg27HQxPddmEL8Blx0jn2BuU/nlwjM/9SnYpEfDrWiAMgLPlB8Q==
+
"@melloware/coloris@^0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@melloware/coloris/-/coloris-0.25.0.tgz#7012c10dc510dca1660b3692ed3c812344aec6ca"
@@ -79,6 +84,13 @@ marked@14.0.0:
resolved "https://registry.yarnpkg.com/marked/-/marked-14.0.0.tgz#79a1477358a59e0660276f8fec76de2c33f35d83"
integrity sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==
+mathjax@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/mathjax/-/mathjax-4.0.0.tgz#ff7aa8b6bfdc7e97e41091a9ed6d8c8f8eefd2a0"
+ integrity sha512-ThMPHiPl9ibZBInAmfoTCNq9MgCdH7ChIQ9YhKFc325noJ4DMzy9/Q14qdcuPzVJjEmC3kyXhwnERZWX3hbWzQ==
+ dependencies:
+ "@mathjax/mathjax-newcm-font" "^4.0.0"
+
monaco-editor@^0.54.0:
version "0.54.0"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.54.0.tgz#c0d6ebb46b83f1bef6f67f6aa471e38ba7ef8231"
From 41959351e471c5c92aab3f7bb0ac5cfd688a0beb Mon Sep 17 00:00:00 2001
From: Michael Vasseur <14887731+vmcj@users.noreply.github.com>
Date: Thu, 30 Oct 2025 19:13:03 +0100
Subject: [PATCH 2/3] Render MathJax in clarifications
---
webapp/public/js/domjudge.js | 1 +
webapp/public/js/tex-chtml.js | 1 +
webapp/public/mathjax | 1 +
webapp/public/mathjaxfonts | 1 +
webapp/templates/base.html.twig | 1 +
webapp/templates/jury/export/layout.html.twig | 1 +
.../templates/partials/mathjaxhead.html.twig | 23 +++++++++++++++++++
webapp/templates/team/index.html.twig | 2 ++
.../partials/clarification_content.html.twig | 4 ++++
.../partials/clarification_list.html.twig | 5 +++-
10 files changed, 39 insertions(+), 1 deletion(-)
create mode 120000 webapp/public/js/tex-chtml.js
create mode 120000 webapp/public/mathjax
create mode 120000 webapp/public/mathjaxfonts
create mode 100644 webapp/templates/partials/mathjaxhead.html.twig
diff --git a/webapp/public/js/domjudge.js b/webapp/public/js/domjudge.js
index 6ba5118d97..38bce348c7 100644
--- a/webapp/public/js/domjudge.js
+++ b/webapp/public/js/domjudge.js
@@ -945,6 +945,7 @@ function previewClarification($input, $previewDiv) {
}
}).done(function (data) {
$previewDiv.html(data.html);
+ MathJax.typesetPromise([document.getElementById($previewDiv[0].id)]);
});
}
}
diff --git a/webapp/public/js/tex-chtml.js b/webapp/public/js/tex-chtml.js
new file mode 120000
index 0000000000..47467ad03c
--- /dev/null
+++ b/webapp/public/js/tex-chtml.js
@@ -0,0 +1 @@
+../../node_modules/mathjax/tex-chtml.js
\ No newline at end of file
diff --git a/webapp/public/mathjax b/webapp/public/mathjax
new file mode 120000
index 0000000000..3a045cb444
--- /dev/null
+++ b/webapp/public/mathjax
@@ -0,0 +1 @@
+../node_modules/mathjax
\ No newline at end of file
diff --git a/webapp/public/mathjaxfonts b/webapp/public/mathjaxfonts
new file mode 120000
index 0000000000..89fd736689
--- /dev/null
+++ b/webapp/public/mathjaxfonts
@@ -0,0 +1 @@
+../node_modules/@mathjax/mathjax-newcm-font/chtml/woff2
\ No newline at end of file
diff --git a/webapp/templates/base.html.twig b/webapp/templates/base.html.twig
index e953356b67..ecf48113bc 100644
--- a/webapp/templates/base.html.twig
+++ b/webapp/templates/base.html.twig
@@ -13,6 +13,7 @@
+ {% include 'partials/mathjaxhead.html.twig' %}
{% for file in customAssetFiles('js') %}
diff --git a/webapp/templates/jury/export/layout.html.twig b/webapp/templates/jury/export/layout.html.twig
index 4e465a2aab..39d6ae36a3 100644
--- a/webapp/templates/jury/export/layout.html.twig
+++ b/webapp/templates/jury/export/layout.html.twig
@@ -108,6 +108,7 @@
color: darkgrey;
}
+ {% include 'partials/mathjaxhead.html.twig' %}
diff --git a/webapp/templates/partials/mathjaxhead.html.twig b/webapp/templates/partials/mathjaxhead.html.twig
new file mode 100644
index 0000000000..128a3fbec0
--- /dev/null
+++ b/webapp/templates/partials/mathjaxhead.html.twig
@@ -0,0 +1,23 @@
+
+
diff --git a/webapp/templates/team/index.html.twig b/webapp/templates/team/index.html.twig
index ae6244810d..cc5e8d3a66 100644
--- a/webapp/templates/team/index.html.twig
+++ b/webapp/templates/team/index.html.twig
@@ -46,6 +46,8 @@
}
$('[data-flash-messages]').html($flash);
+
+ MathJax.typeset([document.getElementsByClassName('clarification-text')]);
}
window.initModalClarificationPreviewAdd = function() {
diff --git a/webapp/templates/team/partials/clarification_content.html.twig b/webapp/templates/team/partials/clarification_content.html.twig
index 7e38c1e53c..a3d2e03e52 100644
--- a/webapp/templates/team/partials/clarification_content.html.twig
+++ b/webapp/templates/team/partials/clarification_content.html.twig
@@ -24,3 +24,7 @@
{{ form_end(form) }}
+
+
diff --git a/webapp/templates/team/partials/clarification_list.html.twig b/webapp/templates/team/partials/clarification_list.html.twig
index a8afe67896..e55b489a85 100644
--- a/webapp/templates/team/partials/clarification_list.html.twig
+++ b/webapp/templates/team/partials/clarification_list.html.twig
@@ -48,7 +48,7 @@
{%- endif -%}
-
+ |
{{ clarification.summary
| markdown_to_html
@@ -57,6 +57,9 @@
| raw
}}
+
|
{%- endfor %}
From b86a52e69cbfca2a637ce6957f23b3d9dd014d06 Mon Sep 17 00:00:00 2001
From: Michael Vasseur <14887731+vmcj@users.noreply.github.com>
Date: Sat, 27 Sep 2025 00:53:44 +0200
Subject: [PATCH 3/3] Use our own markdown_to_html convertor
Which stashes LaTeX during conversion and pops it afterwards.
Otherwise things like `\\` get replaced with `\` which failed with:
https://github.com/DOMjudge/domjudge/pull/3099#issuecomment-3293328981
---
webapp/src/Twig/TwigExtension.php | 39 +++++++++++++++++++
webapp/templates/jury/clarification.html.twig | 2 +-
webapp/templates/jury/config.html.twig | 2 +-
webapp/templates/jury/config_check.html.twig | 2 +-
.../jury/export/clarifications.html.twig | 4 +-
.../team/partials/clarification.html.twig | 2 +-
.../partials/clarification_list.html.twig | 2 +-
7 files changed, 46 insertions(+), 7 deletions(-)
diff --git a/webapp/src/Twig/TwigExtension.php b/webapp/src/Twig/TwigExtension.php
index f233757237..35c6931683 100644
--- a/webapp/src/Twig/TwigExtension.php
+++ b/webapp/src/Twig/TwigExtension.php
@@ -27,6 +27,7 @@
use App\Utils\Utils;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManagerInterface;
+use Ramsey\Uuid\Uuid;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Intl\Countries;
use Symfony\Component\Intl\Exception\MissingResourceException;
@@ -38,12 +39,18 @@
use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\Extension\GlobalsInterface;
+use Twig\Extra\Markdown\MarkdownRuntime;
use Twig\Runtime\EscaperRuntime;
use Twig\TwigFilter;
use Twig\TwigFunction;
class TwigExtension extends AbstractExtension implements GlobalsInterface
{
+ /**
+ * @param array $latexFound
+ */
+ private array $latexFound;
+
/**
* @param array $renderedSources
*/
@@ -126,6 +133,7 @@ public function getFilters(): array
new TwigFilter('medalType', $this->awards->medalType(...)),
new TwigFilter('numTableActions', $this->numTableActions(...)),
new TwigFilter('extensionToMime', $this->extensionToMime(...)),
+ new TwigFilter('domjudgeMarkdownToHtml', $this->domjudgeMarkdownToHTML(...), ['is_safe' => ['html']]),
];
}
@@ -1381,4 +1389,35 @@ public function extensionToMime(string $extension): string
{
return DOMJudgeService::EXTENSION_TO_MIMETYPE[$extension];
}
+
+ /**
+ * Extract all LaTeX code from the given string, sanitize the markdown and
+ * inject the original LaTeX code back so MathJax can render it.
+ */
+ public function domjudgeMarkdownToHTML(string $markdown): string
+ {
+ $latexPlaceholder = Uuid::uuid4()->toString();
+ while (str_contains($markdown, $latexPlaceholder)) {
+ $latexPlaceholder = Uuid::uuid4()->toString();
+ }
+
+ $markdown = preg_replace_callback(
+ '/(\$[\s\S]*?\$)/',
+ function (array $matches) use ($latexPlaceholder) {
+ // Store and replace matches
+ $this->latexFound[] = $matches[1];
+ return $latexPlaceholder;
+ },
+ $markdown
+ );
+
+ /** @var MarkdownRuntime $runtime */
+ $runtime = $this->twig->getRuntime(MarkdownRuntime::class);
+ $markdown = (string)$runtime->convert($markdown);
+
+ return preg_replace_callback(
+ '/'.$latexPlaceholder.'/',
+ fn() => array_shift($this->latexFound), $markdown
+ );
+ }
}
diff --git a/webapp/templates/jury/clarification.html.twig b/webapp/templates/jury/clarification.html.twig
index e7e7c4d24b..70e79957ed 100644
--- a/webapp/templates/jury/clarification.html.twig
+++ b/webapp/templates/jury/clarification.html.twig
@@ -110,7 +110,7 @@
- {{ clar.body | markdown_to_html | sanitize_html('app.clarification_sanitizer') }}
+ {{ clar.body | domjudgeMarkdownToHtml | sanitize_html('app.clarification_sanitizer') }}
diff --git a/webapp/templates/jury/config.html.twig b/webapp/templates/jury/config.html.twig
index f30cc9b4d6..94284b8898 100644
--- a/webapp/templates/jury/config.html.twig
+++ b/webapp/templates/jury/config.html.twig
@@ -218,7 +218,7 @@
{{ errors[option.name] }}
{% endif %}
- {{ option.description | markdown_to_html }}
+ {{ option.description | domjudgeMarkdownToHtml }}
{% endfor %}
diff --git a/webapp/templates/jury/config_check.html.twig b/webapp/templates/jury/config_check.html.twig
index eed1487090..fff0e67809 100644
--- a/webapp/templates/jury/config_check.html.twig
+++ b/webapp/templates/jury/config_check.html.twig
@@ -99,7 +99,7 @@
{% if testresult.escape is not defined or testresult.escape %}
{% set description = description | escape %}
{% endif %}
- {{ description | markdown_to_html }}
+ {{ description | domjudgeMarkdownToHtml }}
diff --git a/webapp/templates/jury/export/clarifications.html.twig b/webapp/templates/jury/export/clarifications.html.twig
index 871033868e..25f7bed29d 100644
--- a/webapp/templates/jury/export/clarifications.html.twig
+++ b/webapp/templates/jury/export/clarifications.html.twig
@@ -98,7 +98,7 @@
| Content |
- {{ clarification.body | markdown_to_html | sanitize_html('app.clarification_sanitizer') }}
+ {{ clarification.body | domjudgeMarkdownToHtml | sanitize_html('app.clarification_sanitizer') }}
|
{% if clarification.replies is not empty %}
@@ -114,7 +114,7 @@
- {{ reply.body | markdown_to_html | sanitize_html('app.clarification_sanitizer') }}
+ {{ reply.body | domjudgeMarkdownToHtml | sanitize_html('app.clarification_sanitizer') }}
|
{% endfor %}
diff --git a/webapp/templates/team/partials/clarification.html.twig b/webapp/templates/team/partials/clarification.html.twig
index 5004e4c0d4..9045ceadde 100644
--- a/webapp/templates/team/partials/clarification.html.twig
+++ b/webapp/templates/team/partials/clarification.html.twig
@@ -40,7 +40,7 @@
- {{ clarification.body | markdown_to_html | sanitize_html('app.clarification_sanitizer') }}
+ {{ clarification.body | domjudgeMarkdownToHtml | sanitize_html('app.clarification_sanitizer') }}
diff --git a/webapp/templates/team/partials/clarification_list.html.twig b/webapp/templates/team/partials/clarification_list.html.twig
index e55b489a85..93de2996c2 100644
--- a/webapp/templates/team/partials/clarification_list.html.twig
+++ b/webapp/templates/team/partials/clarification_list.html.twig
@@ -51,7 +51,7 @@
{{ clarification.summary
- | markdown_to_html
+ | domjudgeMarkdownToHtml
| replace({' ': '', ' ': ''})
| sanitize_html('app.clarification_sanitizer')
| raw
|