Skip to content

Commit

Permalink
Render maths with respect to data-mx-maths
Browse files Browse the repository at this point in the history
(matrix-org/matrix-spec-proposals#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.
  • Loading branch information
NickHu committed Sep 18, 2020
1 parent 162518e commit a6cad60
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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
Expand Down Expand Up @@ -84,6 +86,7 @@ internal abstract class RoomModule {

@Module
companion object {
private val extensions : List<Extension> = listOf(MathsExtension.create())
@Provides
@JvmStatic
@SessionScope
Expand All @@ -94,14 +97,15 @@ internal abstract class RoomModule {
@Provides
@JvmStatic
fun providesParser(): Parser {
return Parser.builder().build()
return Parser.builder().extensions(extensions).build()
}

@Provides
@JvmStatic
fun providesHtmlRenderer(): HtmlRenderer {
return HtmlRenderer
.builder()
.extensions(extensions)
.build()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ internal class MarkdownParser @Inject constructor(
private val htmlRenderer: HtmlRenderer
) {

private val mdSpecialChars = "[`_\\-*>.\\[\\]#~]".toRegex()
private val mdSpecialChars = "[`_\\-*>.\\[\\]#~$]".toRegex()

fun parse(text: String): TextContent {
// If no special char are detected, just return plain text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.glide.GlideApp
import im.vector.app.core.resources.ColorProvider
import im.vector.app.features.home.AvatarRenderer
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonPlugin
import io.noties.markwon.ext.latex.JLatexMathPlugin
Expand All @@ -39,6 +40,13 @@ class EventHtmlRenderer @Inject constructor(context: Context,

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)
Expand Down

0 comments on commit a6cad60

Please sign in to comment.