From 20821fbe802a527c950fe333d649894820a395f7 Mon Sep 17 00:00:00 2001 From: Nick Hu Date: Fri, 18 Sep 2020 15:23:21 +0100 Subject: [PATCH] Render maths with respect to `data-mx-maths` (https://github.com/matrix-org/matrix-doc/pull/2191) Firstly, this implements a commonmark-java plugin which is solely used to parse LaTeX input in the composer box, so that they can be rendered into `fallback` and `
fallback
` for inline and display maths respectively in the sent message. Secondly, received messages of this form are pre-processed by a simple regex into a form which markwon (which performs the rendering) expects. --- .../commonmark/ext/maths/DisplayMaths.java | 32 +++++++++ .../org/commonmark/ext/maths/InlineMaths.java | 55 ++++++++++++++++ .../commonmark/ext/maths/MathsExtension.java | 51 ++++++++++++++ .../DollarMathsDelimiterProcessor.java | 66 +++++++++++++++++++ .../maths/internal/MathsHtmlNodeRenderer.java | 50 ++++++++++++++ .../ext/maths/internal/MathsNodeRenderer.java | 35 ++++++++++ .../sdk/internal/session/room/RoomModule.kt | 6 +- .../session/room/send/MarkdownParser.kt | 2 +- .../app/features/html/EventHtmlRenderer.kt | 8 +++ 9 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/commonmark/ext/maths/DisplayMaths.java create mode 100644 matrix-sdk-android/src/main/java/org/commonmark/ext/maths/InlineMaths.java create mode 100644 matrix-sdk-android/src/main/java/org/commonmark/ext/maths/MathsExtension.java create mode 100644 matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/DollarMathsDelimiterProcessor.java create mode 100644 matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.java create mode 100644 matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsNodeRenderer.java diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/DisplayMaths.java b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/DisplayMaths.java new file mode 100644 index 0000000000..3cf574b824 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/DisplayMaths.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.commonmark.ext.maths; + +import org.commonmark.node.CustomBlock; + +public class DisplayMaths extends CustomBlock { + public enum DisplayDelimiter { + DOUBLE_DOLLAR, + SQUARE_BRACKET_ESCAPED + }; + + private DisplayDelimiter delimiter; + + public DisplayMaths(DisplayDelimiter delimiter) { + this.delimiter = delimiter; + } +} diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/InlineMaths.java b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/InlineMaths.java new file mode 100644 index 0000000000..982570a58d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/InlineMaths.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.commonmark.ext.maths; + +import org.commonmark.node.CustomNode; +import org.commonmark.node.Delimited; + +public class InlineMaths extends CustomNode implements Delimited { + public enum InlineDelimiter { + SINGLE_DOLLAR, + ROUND_BRACKET_ESCAPED + }; + + private InlineDelimiter delimiter; + + public InlineMaths(InlineDelimiter delimiter) { + this.delimiter = delimiter; + } + + @Override + public String getOpeningDelimiter() { + switch (delimiter) { + case SINGLE_DOLLAR: + return "$"; + case ROUND_BRACKET_ESCAPED: + return "\\("; + } + return null; + } + + @Override + public String getClosingDelimiter() { + switch (delimiter) { + case SINGLE_DOLLAR: + return "$"; + case ROUND_BRACKET_ESCAPED: + return "\\)"; + } + return null; + } +} diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/MathsExtension.java b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/MathsExtension.java new file mode 100644 index 0000000000..31706b3f2d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/MathsExtension.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.commonmark.ext.maths; + +import org.commonmark.Extension; +import org.commonmark.ext.maths.internal.DollarMathsDelimiterProcessor; +import org.commonmark.ext.maths.internal.MathsHtmlNodeRenderer; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.NodeRenderer; +import org.commonmark.renderer.html.HtmlNodeRendererContext; +import org.commonmark.renderer.html.HtmlNodeRendererFactory; +import org.commonmark.renderer.html.HtmlRenderer; + +public class MathsExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension { + + private MathsExtension() { + } + + public static Extension create() { + return new MathsExtension(); + } + + @Override + public void extend(Parser.Builder parserBuilder) { + parserBuilder.customDelimiterProcessor(new DollarMathsDelimiterProcessor()); + } + + @Override + public void extend(HtmlRenderer.Builder rendererBuilder) { + rendererBuilder.nodeRendererFactory(new HtmlNodeRendererFactory() { + @Override + public NodeRenderer create(HtmlNodeRendererContext context) { + return new MathsHtmlNodeRenderer(context); + } + }); + } +} diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/DollarMathsDelimiterProcessor.java b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/DollarMathsDelimiterProcessor.java new file mode 100644 index 0000000000..809c070f29 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/DollarMathsDelimiterProcessor.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.commonmark.ext.maths.internal; + +import org.commonmark.ext.maths.DisplayMaths; +import org.commonmark.ext.maths.InlineMaths; +import org.commonmark.node.Node; +import org.commonmark.node.Text; +import org.commonmark.parser.delimiter.DelimiterProcessor; +import org.commonmark.parser.delimiter.DelimiterRun; + +public class DollarMathsDelimiterProcessor implements DelimiterProcessor { + @Override + public char getOpeningCharacter() { + return '$'; + } + + @Override + public char getClosingCharacter() { + return '$'; + } + + @Override + public int getMinLength() { + return 1; + } + + @Override + public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) { + if (opener.length() == 1 && closer.length() == 1) + return 1; // inline + else if (opener.length() == 2 && closer.length() == 2) + return 2; // display + else + return 0; + } + + @Override + public void process(Text opener, Text closer, int delimiterUse) { + Node maths = delimiterUse == 1 ? new InlineMaths(InlineMaths.InlineDelimiter.SINGLE_DOLLAR) : + new DisplayMaths(DisplayMaths.DisplayDelimiter.DOUBLE_DOLLAR); + + Node tmp = opener.getNext(); + while (tmp != null && tmp != closer) { + Node next = tmp.getNext(); + maths.appendChild(tmp); + tmp = next; + } + + opener.insertAfter(maths); + } +} diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.java b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.java new file mode 100644 index 0000000000..a84129cd28 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.commonmark.ext.maths.internal; + +import org.commonmark.ext.maths.DisplayMaths; +import org.commonmark.node.Node; +import org.commonmark.node.Text; +import org.commonmark.renderer.html.HtmlNodeRendererContext; +import org.commonmark.renderer.html.HtmlWriter; + +import java.util.Collections; +import java.util.Map; + +public class MathsHtmlNodeRenderer extends MathsNodeRenderer { + private final HtmlNodeRendererContext context; + private final HtmlWriter html; + + public MathsHtmlNodeRenderer(HtmlNodeRendererContext context) { + this.context = context; + this.html = context.getWriter(); + } + + @Override + public void render(Node node) { + boolean display = node.getClass() == DisplayMaths.class; + Node contents = node.getFirstChild(); // should be the only child + String latex = ((Text) contents).getLiteral(); + Map attributes = context.extendAttributes(node, display ? "div" : "span", Collections.singletonMap("data-mx-maths", + latex)); + html.tag(display ? "div" : "span", attributes); + html.tag("code"); + context.render(contents); + html.tag("/code"); + html.tag(display ? "/div" : "/span"); + } +} diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsNodeRenderer.java b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsNodeRenderer.java new file mode 100644 index 0000000000..4d2a35f33d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsNodeRenderer.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.commonmark.ext.maths.internal; + +import org.commonmark.ext.maths.DisplayMaths; +import org.commonmark.ext.maths.InlineMaths; +import org.commonmark.node.Node; +import org.commonmark.renderer.NodeRenderer; + +import java.util.HashSet; +import java.util.Set; + +abstract class MathsNodeRenderer implements NodeRenderer { + @Override + public Set> getNodeTypes() { + final Set> types = new HashSet>(); + types.add(InlineMaths.class); + types.add(DisplayMaths.class); + return types; + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index dbd0ae6f06..105c8ad03e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -19,6 +19,8 @@ package org.matrix.android.sdk.internal.session.room import dagger.Binds import dagger.Module import dagger.Provides +import org.commonmark.Extension +import org.commonmark.ext.maths.MathsExtension import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer import org.matrix.android.sdk.api.session.file.FileService @@ -104,6 +106,7 @@ internal abstract class RoomModule { @Module companion object { + private val extensions : List = listOf(MathsExtension.create()) @Provides @JvmStatic @SessionScope @@ -121,7 +124,7 @@ internal abstract class RoomModule { @Provides @JvmStatic fun providesParser(): Parser { - return Parser.builder().build() + return Parser.builder().extensions(extensions).build() } @Provides @@ -129,6 +132,7 @@ internal abstract class RoomModule { fun providesHtmlRenderer(): HtmlRenderer { return HtmlRenderer .builder() + .extensions(extensions) .softbreak("
") .build() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt index c99d482300..1ac95154f8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt @@ -32,7 +32,7 @@ internal class MarkdownParser @Inject constructor( private val textPillsUtils: TextPillsUtils ) { - private val mdSpecialChars = "[`_\\-*>.\\[\\]#~]".toRegex() + private val mdSpecialChars = "[`_\\-*>.\\[\\]#~$]".toRegex() fun parse(text: CharSequence): TextContent { val source = textPillsUtils.processSpecialSpansToMarkdown(text) ?: text.toString() diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index cc7c999067..e2fb385ae5 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -20,6 +20,7 @@ import android.content.Context import android.text.Spannable import androidx.core.text.toSpannable import im.vector.app.core.resources.ColorProvider +import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.Markwon import io.noties.markwon.MarkwonPlugin import io.noties.markwon.ext.latex.JLatexMathPlugin @@ -41,6 +42,13 @@ class EventHtmlRenderer @Inject constructor(htmlConfigure: MatrixHtmlPluginConfi private val markwon = Markwon.builder(context) .usePlugin(HtmlPlugin.create(htmlConfigure)) + .usePlugin(object : AbstractMarkwonPlugin() { // Markwon expects maths to be in a specific format: https://noties.io/Markwon/docs/v4/ext-latex + override fun processMarkdown(markdown: String): String { + return markdown + .replace(Regex(""".*?""")) { matchResult -> "$$" + matchResult.groupValues[1] + "$$" } + .replace(Regex(""".*?""")) { matchResult -> "\n$$\n" + matchResult.groupValues[1] + "\n$$\n" } + } + }) .usePlugin(MarkwonInlineParserPlugin.create()) .usePlugin(JLatexMathPlugin.create(44F) { builder -> builder.inlinesEnabled(true)