Skip to content

Commit cc6120a

Browse files
author
Daniil Karpenko
committed
Prototype mermaid embeds in markdown preview
1 parent 9fea726 commit cc6120a

2 files changed

Lines changed: 138 additions & 0 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Build Mermaid APK
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches:
7+
- prototype/markdown-mermaid-embed-android
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
timeout-minutes: 120
13+
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Set up JDK 17
19+
uses: actions/setup-java@v4
20+
with:
21+
distribution: temurin
22+
java-version: '17'
23+
24+
- name: Set up Android SDK
25+
uses: android-actions/setup-android@v3
26+
27+
- name: Install Android packages
28+
run: |
29+
yes | sdkmanager --licenses
30+
sdkmanager \
31+
"platform-tools" \
32+
"platforms;android-35" \
33+
"build-tools;35.0.0" \
34+
"cmake;3.10.2.4988404" \
35+
"ndk;27.2.12479018"
36+
37+
- name: Build standalone debug APK
38+
env:
39+
ANDROID_HOME: ${{ env.ANDROID_SDK_ROOT }}
40+
ANDROID_SDK_ROOT: ${{ env.ANDROID_SDK_ROOT }}
41+
run: |
42+
chmod +x gradlew
43+
./gradlew --stacktrace :TMessagesProj_AppStandalone:assembleAfatDebug
44+
45+
- name: Upload APK
46+
uses: actions/upload-artifact@v4
47+
with:
48+
name: telegram-mermaid-apk
49+
path: TMessagesProj_AppStandalone/build/outputs/apk/**/app.apk
50+
if-no-files-found: error

TMessagesProj/src/main/java/org/telegram/ui/Components/MarkdownParser.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ public class MarkdownParser {
7373

7474
private static final int MAX_RICH_TEXT_LEN = 8192;
7575
private static final int MAX_FILE_SIZE = 64 * 1024;
76+
private static final int MERMAID_EMBED_WIDTH = 720;
77+
private static final int MERMAID_EMBED_HEIGHT = 240;
7678

7779
public static boolean isMarkdown(MessageObject msg) {
7880
if (msg == null) return false;
@@ -900,6 +902,10 @@ public void visit(ThematicBreak thematicBreak) {
900902

901903
@Override
902904
public void visit(FencedCodeBlock fencedCodeBlock) {
905+
if (isMermaidFence(fencedCodeBlock.getInfo())) {
906+
emit(buildMermaidBlock(fencedCodeBlock.getLiteral()));
907+
return;
908+
}
903909
final TLRPC.TL_pageBlockPreformatted b = new TLRPC.TL_pageBlockPreformatted();
904910
b.text = first(plain(fencedCodeBlock.getLiteral()));
905911
b.language = fencedCodeBlock.getInfo() == null ? "" : fencedCodeBlock.getInfo();
@@ -1051,6 +1057,88 @@ private TLRPC.TL_pageTableCell buildTableCell(TableCell cell, boolean header) {
10511057
}
10521058
}
10531059

1060+
private static boolean isMermaidFence(String info) {
1061+
if (TextUtils.isEmpty(info)) {
1062+
return false;
1063+
}
1064+
final String trimmed = info.trim();
1065+
if (trimmed.isEmpty()) {
1066+
return false;
1067+
}
1068+
final int space = trimmed.indexOf(' ');
1069+
final String language = (space >= 0 ? trimmed.substring(0, space) : trimmed).trim();
1070+
return "mermaid".equalsIgnoreCase(language);
1071+
}
1072+
1073+
private static TLRPC.TL_pageBlockEmbed buildMermaidBlock(String literal) {
1074+
final TLRPC.TL_pageBlockEmbed block = new TLRPC.TL_pageBlockEmbed();
1075+
block.html = buildMermaidHtml(literal == null ? "" : literal);
1076+
block.flags |= TLObject.FLAG_2;
1077+
block.w = MERMAID_EMBED_WIDTH;
1078+
block.h = MERMAID_EMBED_HEIGHT;
1079+
block.flags |= TLObject.FLAG_5;
1080+
block.allow_scrolling = false;
1081+
block.full_width = false;
1082+
block.caption = emptyCaption();
1083+
return block;
1084+
}
1085+
1086+
private static TLRPC.TL_pageCaption emptyCaption() {
1087+
final TLRPC.TL_pageCaption caption = new TLRPC.TL_pageCaption();
1088+
caption.text = new TLRPC.TL_textEmpty();
1089+
caption.credit = new TLRPC.TL_textEmpty();
1090+
return caption;
1091+
}
1092+
1093+
private static String buildMermaidHtml(String source) {
1094+
final StringBuilder html = new StringBuilder(source.length() + 2048);
1095+
html.append("<!doctype html><html><head><meta charset=\"utf-8\">");
1096+
html.append("<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1\">");
1097+
html.append("<style>");
1098+
html.append("html,body{margin:0;padding:0;background:#fff;color:#222;font:14px/1.45 sans-serif;overflow:hidden;}");
1099+
html.append(".wrap{padding:12px;box-sizing:border-box;}");
1100+
html.append(".mermaid{display:flex;justify-content:center;}");
1101+
html.append("#error{display:none;white-space:pre-wrap;color:#b00020;padding:12px;font:12px/1.5 monospace;}");
1102+
html.append("</style>");
1103+
html.append("</head><body><div class=\"wrap\">");
1104+
html.append("<pre class=\"mermaid\">");
1105+
appendEscapedHtml(html, source);
1106+
html.append("</pre><div id=\"error\"></div></div>");
1107+
html.append("<script src=\"https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js\"></script>");
1108+
html.append("<script>");
1109+
html.append("function tgResize(){var h=Math.max(document.body.scrollHeight,document.documentElement.scrollHeight,120);");
1110+
html.append("if(window.TelegramWebviewProxy&&window.TelegramWebviewProxy.postEvent){");
1111+
html.append("window.TelegramWebviewProxy.postEvent('resize_frame',JSON.stringify({height:Math.ceil(h)}));}}");
1112+
html.append("window.addEventListener('resize',function(){setTimeout(tgResize,0);});");
1113+
html.append("(async function(){try{");
1114+
html.append("mermaid.initialize({startOnLoad:false,securityLevel:'loose',theme:(window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches)?'dark':'default'});");
1115+
html.append("await mermaid.run({nodes:document.querySelectorAll('.mermaid')});");
1116+
html.append("}catch(e){var err=document.getElementById('error');if(err){err.style.display='block';err.textContent=String(e);}}");
1117+
html.append("setTimeout(tgResize,50);})();");
1118+
html.append("</script></body></html>");
1119+
return html.toString();
1120+
}
1121+
1122+
private static void appendEscapedHtml(StringBuilder out, String source) {
1123+
for (int i = 0; i < source.length(); i++) {
1124+
final char ch = source.charAt(i);
1125+
switch (ch) {
1126+
case '&':
1127+
out.append("&amp;");
1128+
break;
1129+
case '<':
1130+
out.append("&lt;");
1131+
break;
1132+
case '>':
1133+
out.append("&gt;");
1134+
break;
1135+
default:
1136+
out.append(ch);
1137+
break;
1138+
}
1139+
}
1140+
}
1141+
10541142
public static class RichTextParser extends AbstractVisitor {
10551143

10561144
private static final int MAX_BLOCK_DEPTH = 64;

0 commit comments

Comments
 (0)