From 01a5300be88665ae414c15e8e0667c93799d8f0e Mon Sep 17 00:00:00 2001 From: dakkar Date: Sat, 18 Jan 2025 12:51:38 +0000 Subject: [PATCH 1/3] handle more complex ruby from/to html - fixes #605 this is not exactly great, but it should be "good enough" note that the new `group` function should not escape in the wild, as we don't document it and only use it internally I tried using `$[scale foo bar]` instead of `$[group foo bar]`, but that would be rendered as `foo bar` when sent over the network to non-misskey instances, and we don't want that --- packages/backend/src/core/MfmService.ts | 67 +++++++++++++++++++ packages/backend/test/unit/MfmService.ts | 13 ++++ .../frontend/src/components/global/MkMfm.ts | 4 ++ 3 files changed, 84 insertions(+) diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 42676d6f98..48995672c5 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -230,6 +230,67 @@ export class MfmService { break; } + case 'rp': break + case 'rt': { + appendChildren(node.childNodes); + break; + } + case 'ruby': { + if (node.childNodes) { + /* + we get: + ``` + + some text ( annotation ) + more text more annotation + + ``` + + and we want to produce: + ``` + $[ruby $[group some text] annotation] + $[ruby $[group more text] more annotation] + ``` + + that `group` is a hack, because when the `ruby` render + sees just text inside the `$[ruby]`, it splits on + whitespace, considers the first "word" to be the main + content, and the rest the annotation + + with that `group`, we force it to consider the whole + group as the main content + + (note that the `rp` are to be ignored, they only exist + for browsers who don't understand ruby) + */ + let nonRtNodes=[]; + // scan children, ignore `rp`, split on `rt` + for (const child of node.childNodes) { + if (treeAdapter.isTextNode(child)) { + nonRtNodes.push(child); + continue; + } + if (!treeAdapter.isElementNode(child)) { + continue; + } + if (child.nodeName == 'rp') { + continue; + } + if (child.nodeName == 'rt') { + text += '$[ruby $[group '; + appendChildren(nonRtNodes); + text += '] '; + analyze(child); + text += '] '; + nonRtNodes=[]; + continue; + } + nonRtNodes.push(child); + } + } + break; + } + default: // includes inline elements { appendChildren(node.childNodes); @@ -348,6 +409,12 @@ export class MfmService { } } + case 'group': { // this is mostly a hack for `ruby` + const el = doc.createElement('span'); + appendChildren(node.children, el); + return el; + } + default: { return fnDefault(node); } diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts index 8d5683329f..93ce0672dc 100644 --- a/packages/backend/test/unit/MfmService.ts +++ b/packages/backend/test/unit/MfmService.ts @@ -45,6 +45,12 @@ describe('MfmService', () => { const output = '

<p>Hello, world!</p>

'; assert.equal(mfmService.toHtml(mfm.parse(input)), output); }); + + test('ruby', () => { + const input = '$[ruby $[group *some* text] ignore me]'; + const output = '

some text(ignore me)

'; + assert.equal(mfmService.toHtml(mfm.parse(input)), output); + }); }); describe('fromHtml', () => { @@ -115,5 +121,12 @@ describe('MfmService', () => { test('hashtag', () => { assert.deepStrictEqual(mfmService.fromHtml('

a #a d

', ['#a']), 'a #a d'); }); + + test('ruby', () => { + assert.deepStrictEqual( + mfmService.fromHtml(' some text (ignore me) and more'), + '$[ruby $[group some text ] ignore me] $[ruby $[group and ] more]' + ); + }); }); }); diff --git a/packages/frontend/src/components/global/MkMfm.ts b/packages/frontend/src/components/global/MkMfm.ts index 1039572a06..aceed17189 100644 --- a/packages/frontend/src/components/global/MkMfm.ts +++ b/packages/frontend/src/components/global/MkMfm.ts @@ -358,6 +358,10 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext Date: Sat, 18 Jan 2025 14:54:17 +0000 Subject: [PATCH 2/3] pick lints --- packages/backend/src/core/MfmService.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 48995672c5..bc624daaee 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -230,7 +230,7 @@ export class MfmService { break; } - case 'rp': break + case 'rp': break; case 'rt': { appendChildren(node.childNodes); break; @@ -263,7 +263,7 @@ export class MfmService { (note that the `rp` are to be ignored, they only exist for browsers who don't understand ruby) */ - let nonRtNodes=[]; + let nonRtNodes = []; // scan children, ignore `rp`, split on `rt` for (const child of node.childNodes) { if (treeAdapter.isTextNode(child)) { @@ -273,16 +273,16 @@ export class MfmService { if (!treeAdapter.isElementNode(child)) { continue; } - if (child.nodeName == 'rp') { + if (child.nodeName === 'rp') { continue; } - if (child.nodeName == 'rt') { + if (child.nodeName === 'rt') { text += '$[ruby $[group '; appendChildren(nonRtNodes); text += '] '; analyze(child); text += '] '; - nonRtNodes=[]; + nonRtNodes = []; continue; } nonRtNodes.push(child); From 408e2f824a686defd3ea96cc5782b6b59779b7b9 Mon Sep 17 00:00:00 2001 From: dakkar Date: Sun, 19 Jan 2025 11:15:01 +0000 Subject: [PATCH 3/3] format ruby for masto api --- packages/backend/src/core/MfmService.ts | 68 +++++++++++++++++++++--- packages/backend/test/unit/MfmService.ts | 38 +++++++++++++ 2 files changed, 100 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index bc624daaee..dc47e38562 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -409,7 +409,9 @@ export class MfmService { } } - case 'group': { // this is mostly a hack for `ruby` + // hack for ruby, should never be needed because we should + // never send this out to other instances + case 'group': { const el = doc.createElement('span'); appendChildren(node.children, el); return el; @@ -593,11 +595,65 @@ export class MfmService { }, async fn(node) { - const el = doc.createElement('span'); - el.textContent = '*'; - await appendChildren(node.children, el); - el.textContent += '*'; - return el; + switch (node.props.name) { + case 'group': { // hack for ruby + const el = doc.createElement('span'); + await appendChildren(node.children, el); + return el; + } + case 'ruby': { + if (node.children.length === 1) { + const child = node.children[0]; + const text = child.type === 'text' ? child.props.text : ''; + const rubyEl = doc.createElement('ruby'); + const rtEl = doc.createElement('rt'); + + const rpStartEl = doc.createElement('rp'); + rpStartEl.appendChild(doc.createTextNode('(')); + const rpEndEl = doc.createElement('rp'); + rpEndEl.appendChild(doc.createTextNode(')')); + + rubyEl.appendChild(doc.createTextNode(text.split(' ')[0])); + rtEl.appendChild(doc.createTextNode(text.split(' ')[1])); + rubyEl.appendChild(rpStartEl); + rubyEl.appendChild(rtEl); + rubyEl.appendChild(rpEndEl); + return rubyEl; + } else { + const rt = node.children.at(-1); + + if (!rt) { + const el = doc.createElement('span'); + await appendChildren(node.children, el); + return el; + } + + const text = rt.type === 'text' ? rt.props.text : ''; + const rubyEl = doc.createElement('ruby'); + const rtEl = doc.createElement('rt'); + + const rpStartEl = doc.createElement('rp'); + rpStartEl.appendChild(doc.createTextNode('(')); + const rpEndEl = doc.createElement('rp'); + rpEndEl.appendChild(doc.createTextNode(')')); + + await appendChildren(node.children.slice(0, node.children.length - 1), rubyEl); + rtEl.appendChild(doc.createTextNode(text.trim())); + rubyEl.appendChild(rpStartEl); + rubyEl.appendChild(rtEl); + rubyEl.appendChild(rpEndEl); + return rubyEl; + } + } + + default: { + const el = doc.createElement('span'); + el.textContent = '*'; + await appendChildren(node.children, el); + el.textContent += '*'; + return el; + } + } }, blockCode(node) { diff --git a/packages/backend/test/unit/MfmService.ts b/packages/backend/test/unit/MfmService.ts index 93ce0672dc..5c3ffba422 100644 --- a/packages/backend/test/unit/MfmService.ts +++ b/packages/backend/test/unit/MfmService.ts @@ -47,12 +47,50 @@ describe('MfmService', () => { }); test('ruby', () => { + const input = '$[ruby some text ignore me]'; + const output = '

some(text)

'; + assert.equal(mfmService.toHtml(mfm.parse(input)), output); + }); + + test('ruby2', () => { + const input = '$[ruby *some text* ignore me]'; + const output = '

some text(ignore me)

'; + assert.equal(mfmService.toHtml(mfm.parse(input)), output); + }); + + test('ruby 3', () => { const input = '$[ruby $[group *some* text] ignore me]'; const output = '

some text(ignore me)

'; assert.equal(mfmService.toHtml(mfm.parse(input)), output); }); }); + describe('toMastoApiHtml', () => { + test('br', async () => { + const input = 'foo\nbar\nbaz'; + const output = '

foo
bar
baz

'; + assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output); + }); + + test('br alt', async () => { + const input = 'foo\r\nbar\rbaz'; + const output = '

foo
bar
baz

'; + assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output); + }); + + test('escape', async () => { + const input = '```\n

Hello, world!

\n```'; + const output = '

<p>Hello, world!</p>

'; + assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output); + }); + + test('ruby', async () => { + const input = '$[ruby $[group *some* text] ignore me]'; + const output = '

*some* text(ignore me)

'; + assert.equal(await mfmService.toMastoApiHtml(mfm.parse(input)), output); + }); + }); + describe('fromHtml', () => { test('p', () => { assert.deepStrictEqual(mfmService.fromHtml('

a

b

'), 'a\n\nb');