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
`<span data-mx-maths=...>fallback</span>` and `<div
data-mx-maths=...>fallback</div>` 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.
This commit is contained in:
Nick Hu 2020-09-18 15:23:21 +01:00
parent 78b870c558
commit 20821fbe80
No known key found for this signature in database
GPG Key ID: 9E35DDA3DF631330
9 changed files with 303 additions and 2 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
});
}
}

View File

@ -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);
}
}

View File

@ -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<String, String> attributes = context.extendAttributes(node, display ? "div" : "span", Collections.<String, String>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");
}
}

View File

@ -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<Class<? extends Node>> getNodeTypes() {
final Set<Class<? extends Node>> types = new HashSet<Class<? extends Node>>();
types.add(InlineMaths.class);
types.add(DisplayMaths.class);
return types;
}
}

View File

@ -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<Extension> = 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("<br />")
.build()
}

View File

@ -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()

View File

@ -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("""<span\s+data-mx-maths="([^"]*)">.*?</span>""")) { matchResult -> "$$" + matchResult.groupValues[1] + "$$" }
.replace(Regex("""<div\s+data-mx-maths="([^"]*)">.*?</div>""")) { matchResult -> "\n$$\n" + matchResult.groupValues[1] + "\n$$\n" }
}
})
.usePlugin(MarkwonInlineParserPlugin.create())
.usePlugin(JLatexMathPlugin.create(44F) { builder ->
builder.inlinesEnabled(true)