TINY-11755: Retain semantics if annotations are allowed (#10122)

Related Ticket: TINY-11755

Description of Changes:
* Fixed the issue with `semantics` being removed even when `annotation` elements was allowed
* Also changed so that the `annotation` element is properly removed if it's not allowed so that it wouldn't get rendered.
* The `semantics` element is unwrapped if it's not allowed that should work since the first one should be equation and we want to keep that.

Pre-checks:
* [x] Changelog entry added
* [x] Tests have been added (if applicable)
* [x] Branch prefixed with `feature/`, `hotfix/` or `spike/`

Review:
* [x] Milestone set
* [x] Docs ticket created (if applicable)

GitHub issues (if applicable):


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Bug Fixes**
	- Resolved an issue with MathML element handling, specifically ensuring the `semantics` element is properly retained when annotation elements are allowed.
	- Improved parsing and sanitization of MathML annotations based on specified encoding rules.

- **Tests**
	- Added new test cases to verify MathML annotation handling under different configuration scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
spocke
2025-02-03 15:23:30 +01:00
committed by GitHub
parent b278a0c3a1
commit feefc0d793
4 changed files with 101 additions and 39 deletions

View File

@ -0,0 +1,7 @@
project: tinymce
kind: Fixed
body: The `semantics` element in MathML was not properly retained when `annotation`
elements was allowed.
time: 2025-01-29T08:43:41.042875+01:00
custom:
Issue: TINY-11755

View File

@ -232,15 +232,25 @@ const sanitizeMathmlElement = (node: Element, settings: DomParserSettings) => {
};
const purify = createDompurify();
const allowedEncodings = settings.allow_mathml_annotation_encodings;
const hasAllowedEncodings = Type.isArray(allowedEncodings) && allowedEncodings.length > 0;
const hasValidEncoding = (el: Element) => {
const encoding = el.getAttribute('encoding');
return hasAllowedEncodings && Type.isString(encoding) && Arr.contains(allowedEncodings, encoding);
};
purify.addHook('uponSanitizeElement', (node, evt) => {
const lcTagName = evt.tagName ?? node.nodeName.toLowerCase();
const allowedEncodings = settings.allow_mathml_annotation_encodings;
if (lcTagName === 'annotation' && Type.isArray(allowedEncodings) && allowedEncodings.length > 0) {
const encoding = node.getAttribute('encoding');
if (Type.isString(encoding) && Arr.contains(allowedEncodings, encoding)) {
evt.allowedTags[lcTagName] = true;
if (hasAllowedEncodings && lcTagName === 'semantics') {
evt.allowedTags[lcTagName] = true;
}
if (lcTagName === 'annotation') {
const keepElement = hasValidEncoding(node);
evt.allowedTags[lcTagName] = keepElement;
if (!keepElement) {
node.remove();
}
}
});

View File

@ -724,45 +724,83 @@ describe('browser.tinymce.core.content.EditorContentTest', () => {
});
context('math elements', () => {
const hook = TinyHooks.bddSetupLight<Editor>({
base_url: '/project/tinymce/js/tinymce',
custom_elements: 'math',
allow_mathml_annotation_encodings: [
'application/x-tex',
'application/custom',
'wiris'
]
}, []);
context('annotations encodings defined', () => {
const hook = TinyHooks.bddSetupLight<Editor>({
base_url: '/project/tinymce/js/tinymce',
custom_elements: 'math',
allow_mathml_annotation_encodings: [
'application/x-tex',
'application/custom',
'wiris'
]
}, []);
it('TINY-11166: allow_mathml_annotation_encodings should retain the specified annotation elements', () => {
const editor = hook.editor();
it('TINY-11166: allow_mathml_annotation_encodings should retain the specified annotation elements and the parent semantics element', () => {
const editor = hook.editor();
const input = [
'<div>',
'<math><annotation encoding="application/x-tex">\\frac{1}{2}</annotation></math>',
'<math><annotation encoding="application/custom">custom</annotation></math>',
'<math><annotation encoding="application/custom" src="foo">custom with src</annotation></math>',
'<math><annotation encoding="wiris">{"version":"1.1","math":"&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;mfrac&gt;&lt;mn&gt;1&lt;/mn&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/mfrac&gt;&lt;/math&gt;"}</annotation></math>',
'<math><annotation encoding="text/html">html</annotation></math>',
'<math><annotation encoding="text/svg">svg</annotation></math>',
'</div>'
].join('');
const input = [
'<div>',
'<math><semantics><mn>1</mn><annotation encoding="application/x-tex">\\frac{1}{2}</annotation></semantics></math>',
'<math><semantics><mn>1</mn><annotation encoding="application/custom">custom</annotation></semantics></math>',
'<math><semantics><mn>1</mn><annotation encoding="application/custom" src="foo">custom with src</annotation></semantics></math>',
'<math><semantics><mn>1</mn><annotation encoding="wiris">{"version":"1.1","math":"&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;mn&gt;1&gt;&lt;/mn&gt;&lt;/math&gt;"}</annotation></semantics></math>',
'<math><semantics><mn>1</mn><annotation encoding="text/html">html</annotation></semantics></math>',
'<math><semantics><mn>1</mn><annotation encoding="text/svg">svg</annotation></semantics></math>',
'</div>'
].join('');
editor.setContent(input);
editor.setContent(input);
const expected = [
'<div>',
'<math><annotation encoding="application/x-tex">\\frac{1}{2}</annotation></math>',
'<math><annotation encoding="application/custom">custom</annotation></math>',
'<math><annotation encoding="application/custom">custom with src</annotation></math>',
'<math><annotation encoding="wiris">{"version":"1.1","math":"&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;mfrac&gt;&lt;mn&gt;1&lt;/mn&gt;&lt;mn&gt;2&lt;/mn&gt;&lt;/mfrac&gt;&lt;/math&gt;"}</annotation></math>',
'<math>html</math>',
'<math>svg</math>',
'</div>'
].join('');
const expected = [
'<div>',
'<math><semantics><mn>1</mn><annotation encoding="application/x-tex">\\frac{1}{2}</annotation></semantics></math>',
'<math><semantics><mn>1</mn><annotation encoding="application/custom">custom</annotation></semantics></math>',
'<math><semantics><mn>1</mn><annotation encoding="application/custom">custom with src</annotation></semantics></math>',
'<math><semantics><mn>1</mn><annotation encoding="wiris">{"version":"1.1","math":"&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;mn&gt;1&gt;&lt;/mn&gt;&lt;/math&gt;"}</annotation></semantics></math>',
'<math><semantics><mn>1</mn></semantics></math>',
'<math><semantics><mn>1</mn></semantics></math>',
'</div>'
].join('');
TinyAssertions.assertContent(editor, expected);
TinyAssertions.assertContent(editor, expected);
});
});
context('annotations encodings not defined', () => {
const hook = TinyHooks.bddSetupLight<Editor>({
base_url: '/project/tinymce/js/tinymce',
custom_elements: 'math'
}, []);
it('TINY-11166: not setting allow_mathml_annotation_encodings should not retain the semantics or annotation elements', () => {
const editor = hook.editor();
const input = [
'<div>',
'<math><semantics><mn>1</mn><annotation encoding="application/x-tex">\\frac{1}{2}</annotation></semantics></math>',
'<math><semantics><mn>1</mn><annotation encoding="application/custom">custom</annotation></semantics></math>',
'<math><semantics><mn>1</mn><annotation encoding="application/custom" src="foo">custom with src</annotation></semantics></math>',
'<math><semantics><mn>1</mn><annotation encoding="wiris">{"version":"1.1","math":"&lt;math xmlns="http://www.w3.org/1998/Math/MathML"&gt;&lt;mn&gt;1&gt;&lt;/mn&gt;&lt;/math&gt;"}</annotation></semantics></math>',
'<math><semantics><mn>1</mn><annotation encoding="text/html">html</annotation></semantics></math>',
'<math><semantics><mn>1</mn><annotation encoding="text/svg">svg</annotation></semantics></math>',
'</div>'
].join('');
editor.setContent(input);
const expected = [
'<div>',
'<math><mn>1</mn></math>',
'<math><mn>1</mn></math>',
'<math><mn>1</mn></math>',
'<math><mn>1</mn></math>',
'<math><mn>1</mn></math>',
'<math><mn>1</mn></math>',
'</div>'
].join('');
TinyAssertions.assertContent(editor, expected);
});
});
});
});

View File

@ -1748,6 +1748,13 @@ describe('browser.tinymce.core.html.DomParserTest', () => {
assert.equal(serializedHtml, '<div><math> <mrow> </mrow> </math> <math> </math></div>');
});
it('TINY-11755: Should retain semantics and annotations if allow_mathml_annotation_encodings is set', () => {
const schema = Schema();
schema.addValidElements('math[*]');
const input = '<math><semantics><annotation encoding="-x-custom-mime">annotation1</annotation><annotation encoding="text/html">annotation2</annotation></semantics></math>';
const serializedHtml = HtmlSerializer({}, schema).serialize(DomParser({ allow_mathml_annotation_encodings: [ '-x-custom-mime' ] }, schema).parse(input));
assert.equal(serializedHtml, '<math><semantics><annotation encoding="-x-custom-mime">annotation1</annotation></semantics></math>');
});
});
context('Special elements', () => {