From a74cc1401db64995332eb875cf1fd55148e35ed5 Mon Sep 17 00:00:00 2001
From: Hong Minhee <hong@minhee.org>
Date: Tue, 10 Dec 2024 18:28:31 +0900
Subject: [PATCH] fix(backend): Let MfmService.fromHtml accept ruby
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This fix makes `MfmService.fromHtml()` method accept `<ruby>` tags
and translate it to MFM's ruby characters syntax (`$[ruby ...]`).

このパッチは`MfmService.fromHtml()`メソッドが`<ruby>`タグをMFMの
読み仮名(ルビ)文法に翻訳する様に修正します。
---
 CHANGELOG.md                             |  1 +
 packages/backend/src/core/MfmService.ts  | 33 ++++++++++++++++++++++++
 packages/backend/test/unit/MfmService.ts | 18 +++++++++++++
 3 files changed, 52 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b9ae480af..2cbe412202 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@
 - Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 )
 - Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正  
   (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737)
+- Fix: 非Misskey系のソフトウェアからHTML`<ruby>`タグを含むノートを受信した場合、MFMの読み仮名(ルビ)文法に変換して表示
 
 
 ## 2024.11.0
diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts
index 8061622340..bf06d4457e 100644
--- a/packages/backend/src/core/MfmService.ts
+++ b/packages/backend/src/core/MfmService.ts
@@ -171,6 +171,39 @@ export class MfmService {
 					break;
 				}
 
+				case 'ruby': {
+					let ruby: [string, string][] = [];
+					for (const child of node.childNodes) {
+						if (child.nodeName === 'rp') {
+							continue;
+						}
+						if (treeAdapter.isTextNode(child) && !/\s|\[|\]/.test(child.value)) {
+							ruby.push([child.value, '']);
+							continue;
+						}
+						if (child.nodeName === 'rt' && ruby.length > 0) {
+							const rt = getText(child);
+							if (/\s|\[|\]/.test(rt)) {
+								// If any space is included in rt, it is treated as a normal text
+								ruby = [];
+								appendChildren(node.childNodes);
+								break;
+							} else {
+								ruby.at(-1)![1] = rt;
+								continue;
+							}
+						}
+						// If any other element is included in ruby, it is treated as a normal text
+						ruby = [];
+						appendChildren(node.childNodes);
+						break;
+					}
+					for (const [base, rt] of ruby) {
+						text += `$[ruby ${base} ${rt}]`;
+					}
+					break;
+				}
+
 				// block code (<pre><code>)
 				case 'pre': {
 					if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts
index fd4a03413b..36af8823f6 100644
--- a/packages/backend/test/unit/MfmService.ts
+++ b/packages/backend/test/unit/MfmService.ts
@@ -108,6 +108,24 @@ describe('MfmService', () => {
 			assert.deepStrictEqual(mfmService.fromHtml('<p>a <a></a> d</p>'), 'a  d');
 		});
 
+		test('ruby', () => {
+			assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp></ruby> b</p>'), 'a $[ruby Misskey ミスキー] b');
+			assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp></ruby> b</p>'), 'a $[ruby Misskey ミスキー]$[ruby Misskey ミスキー] b');
+		});
+
+		test('ruby with spaces', () => {
+			assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Miss key<rp>(</rp><rt>ミスキー</rt><rp>)</rp> b</ruby> c</p>'), 'a Miss key(ミスキー) b c');
+			assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミス キー</rt><rp>)</rp> b</ruby> c</p>'), 'a Misskey(ミス キー) b c');
+			assert.deepStrictEqual(
+				mfmService.fromHtml('<p>a <ruby>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミス キー</rt><rp>)</rp>Misskey<rp>(</rp><rt>ミスキー</rt><rp>)</rp></ruby> b</p>'),
+				'a Misskey(ミスキー)Misskey(ミス キー)Misskey(ミスキー) b'
+			);
+		});
+
+		test('ruby with other inline tags', () => {
+			assert.deepStrictEqual(mfmService.fromHtml('<p>a <ruby><strong>Misskey</strong><rp>(</rp><rt>ミスキー</rt><rp>)</rp> b</ruby> c</p>'), 'a **Misskey**(ミスキー) b c');
+		});
+
 		test('mention', () => {
 			assert.deepStrictEqual(mfmService.fromHtml('<p>a <a href="https://example.com/@user" class="u-url mention">@user</a> d</p>'), 'a @user@example.com d');
 		});