From 8540f32c4b6a42a310e6c4f5f1b2e90d0e1bec3d Mon Sep 17 00:00:00 2001 From: Maiko Sinkyaet Tan Date: Sat, 5 Oct 2024 11:06:39 +0800 Subject: [PATCH] fix(api): `
` and `

` linebreak not correctly rendered (#10) * fix: `
` and `

` linebreak not correctly rendered * refa: remove reduce function * refa: use placeholder for linebreak implementation * fix: should handle continuation of p element * fix: dangling paragraph start mark not correctly handled * fix: align with browser behaviour for `

` and `
` * fix: remove redundant process --- .../src/api/message/create/messager.ts | 111 ++++++++++-------- .../text/__snapshots__/index.test.ts.snap | 10 -- .../messager/fixtures/text/index.test.ts | 1 + .../engine-chronocat-api/tests/mocks/index.ts | 1 + 4 files changed, 64 insertions(+), 59 deletions(-) diff --git a/packages/engine-chronocat-api/src/api/message/create/messager.ts b/packages/engine-chronocat-api/src/api/message/create/messager.ts index d11b864..797b28e 100644 --- a/packages/engine-chronocat-api/src/api/message/create/messager.ts +++ b/packages/engine-chronocat-api/src/api/message/create/messager.ts @@ -1,5 +1,5 @@ import type { Peer, Element as RedElement, RedMessage } from '@chronocat/red' -import { AtType, ChatType, FaceType } from '@chronocat/red' +import { ChatType, FaceType } from '@chronocat/red' import type { ChronocatContext, ChronocatSatoriServerConfig, @@ -13,6 +13,13 @@ import type { Common } from '../../../common' import { r } from './r' import { unlink } from 'node:fs/promises' +const placeholders = { + br: '\u0099BR\u009c', + pStart: '\u0099PSTART\u009c', + pEnd: '\u0099PEND\u009c', + pGeneral: '\u0099P\u009c', +} + class State { constructor(public type: 'message') {} } @@ -61,6 +68,11 @@ export class Messager { } flush = async () => { + if (this.text) { + this.children.push(r.text(this.text)) + this.text = '' + } + if (!this.children.length) return this.normalize() @@ -81,60 +93,76 @@ export class Messager { } private normalize = () => { - this.children = this.children.reduce[]>( - (acc, cur) => { - const last = acc[acc.length - 1] - if ( - cur.textElement && - cur.textElement?.content && - cur.textElement?.atType === AtType.None && - last?.textElement && - last?.textElement?.content && - last?.textElement?.atType === AtType.None - ) { - last.textElement.content += cur.textElement.content - } else { - acc.push(cur) - } - return acc - }, - [], - ) + this.children.forEach((x, idx) => { + const isFirst = idx === 0 + const isLast = idx === this.children.length - 1 + if (x.textElement?.content) { + x.textElement.content = x.textElement.content + // 合并连续段落起始标记和结束标记 + .replace(new RegExp(placeholders.pStart + '{2,}', 'g'), placeholders.pStart) + .replace(new RegExp(placeholders.pEnd + '{2,}', 'g'), placeholders.pEnd) + // 空段落转换为换行 + .replace(new RegExp(placeholders.pStart + placeholders.pEnd + '*', 'g'), placeholders.pGeneral) + // 合并连续段落 + .replace(new RegExp(placeholders.pEnd + placeholders.pStart + '*', 'g'), placeholders.pGeneral) + // 硬换行符,但段落末尾的 br 不渲染 + // 先把 br + pEnd 替换为单独的 pEnd + .replace(new RegExp(`${placeholders.br}${placeholders.pEnd}`, 'g'), placeholders.pEnd) + // 再把 br 替换为换行 + .replace(new RegExp(placeholders.br, 'g'), '\n') - if (this.children[this.children.length - 1]?.textElement?.content === '\n') - this.children.pop() + if (isFirst) { + x.textElement.content = x.textElement.content + // 如果是第一条消息元素,删除最前面的段落标记 + .replace(new RegExp(`^(${placeholders.pGeneral}|${placeholders.pStart}|${placeholders.pEnd})+`, 'g'), '') + } else if (isLast) { + x.textElement.content = x.textElement.content + // 如果是最后一条消息元素,删除最后面的段落标记 + .replace(new RegExp(`(${placeholders.pGeneral}|${placeholders.pStart}|${placeholders.pEnd})+$`, 'g'), '') + } + + // 最后的兜底,替换剩余的段落标记为换行 + x.textElement.content = x.textElement.content + .replace(new RegExp(`(${placeholders.pGeneral}|${placeholders.pStart}|${placeholders.pEnd})+`, 'g'), '\n') + } + }) + const last = this.children[this.children.length - 1] + if (last?.textElement?.content) { + last.textElement.content = last.textElement.content.trimEnd() + } return this.children } - private isEndLine = false + private text = '' visit = async (element: h) => { const { type, attrs, children } = element + if (!['text', 'br', 'p'].includes(type)) { + if (this.text) { + this.children.push(r.text(this.text)) + this.text = '' + } + } + switch (type) { case 'text': { // 文本消息 - this.children.push(r.text(attrs['content'] as string)) - this.isEndLine = false + this.text += attrs['content'] as string return } case 'br': { // 换行 - if (this.isEndLine) { - this.children.push(r.text('\n')) - } - this.isEndLine = false + this.text += placeholders.br return } case 'p': { // 文本段落 - if (this.isEndLine) { - this.children.push(r.text('\n')) - } + this.text += placeholders.pStart await this.render(children) - this.isEndLine = true + this.text += placeholders.pEnd return } @@ -143,9 +171,8 @@ export class Messager { const url = attrs['href'] as string await this.render(children) if (url) { - this.children.push(r.text(` ( ${url} )`)) + this.text += ` ( ${url} )` } - this.isEndLine = false return } @@ -185,7 +212,6 @@ export class Messager { } this.children.push(r.remoteImage(result, picType)) - this.isEndLine = false return } @@ -226,7 +252,6 @@ export class Messager { 1, ), ) - this.isEndLine = false return } else { @@ -246,7 +271,6 @@ export class Messager { 1, ), ) - this.isEndLine = false return } @@ -290,7 +314,6 @@ export class Messager { encodeResult.waveAmplitudes, ), ) - this.isEndLine = false return } catch (cause) { @@ -327,7 +350,6 @@ export class Messager { 1, ), ) - this.isEndLine = false return } @@ -351,7 +373,6 @@ export class Messager { fileMime: attrs['chronocat:mime'] as string | undefined, }) this.children.push(r.remoteFile(result)) - this.isEndLine = false return } @@ -364,8 +385,6 @@ export class Messager { r.at(this.ctx, attrs['name'] as string, attrs['id'] as string), ) } - - this.isEndLine = false return } @@ -399,8 +418,6 @@ export class Messager { ), ) } - - this.isEndLine = false return } @@ -412,8 +429,6 @@ export class Messager { attrs['key'] as string, ), ) - - this.isEndLine = false return } @@ -439,7 +454,6 @@ export class Messager { (author?.attrs['id'] as string | undefined) || undefined, ), ) - this.isEndLine = false return } @@ -488,7 +502,6 @@ export class Messager { default: { // 兜底 await this.render(children) - this.isEndLine = false return } } diff --git a/packages/engine-chronocat-api/tests/messager/fixtures/text/__snapshots__/index.test.ts.snap b/packages/engine-chronocat-api/tests/messager/fixtures/text/__snapshots__/index.test.ts.snap index d16373a..ee03de3 100644 --- a/packages/engine-chronocat-api/tests/messager/fixtures/text/__snapshots__/index.test.ts.snap +++ b/packages/engine-chronocat-api/tests/messager/fixtures/text/__snapshots__/index.test.ts.snap @@ -52,24 +52,18 @@ exports[`Red 编码器应当正确编码 br/p 混搭换行 1`] = ` "content": "aaa bbb uuu - ccc ddd - eee - vvv www fff ggg - hhh iii - xxx yyy - zzz", }, }, @@ -97,15 +91,11 @@ exports[`Red 编码器应当正确编码 p 换行 1`] = ` "atUid": "", "content": "aaa bbb - ccc ddd - eee - fff ggg - hhh iii", }, diff --git a/packages/engine-chronocat-api/tests/messager/fixtures/text/index.test.ts b/packages/engine-chronocat-api/tests/messager/fixtures/text/index.test.ts index 125a551..ef14588 100644 --- a/packages/engine-chronocat-api/tests/messager/fixtures/text/index.test.ts +++ b/packages/engine-chronocat-api/tests/messager/fixtures/text/index.test.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import type { RedMessage } from '@chronocat/red' import h from '@satorijs/element' import { Messager } from '../../../../src/api/message/create/messager' diff --git a/packages/engine-chronocat-api/tests/mocks/index.ts b/packages/engine-chronocat-api/tests/mocks/index.ts index d0f4ac4..3dba0e9 100644 --- a/packages/engine-chronocat-api/tests/mocks/index.ts +++ b/packages/engine-chronocat-api/tests/mocks/index.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import type { ChronocatContext } from '@chronocat/shell' import h from '@satorijs/element'