From 78876299547967c7bc460f215dfb345d6589cbb2 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sat, 17 Dec 2022 21:01:20 +0000 Subject: [PATCH] fix: don't parse rich content in display name (#449) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 三咲智子 --- components/account/AccountHeader.vue | 1 + components/content/ContentRich.setup.ts | 8 +++- composables/content-parse.ts | 64 +++++++++++++------------ composables/content-render.ts | 7 ++- tests/content-rich.test.ts | 2 +- 5 files changed, 47 insertions(+), 35 deletions(-) diff --git a/components/account/AccountHeader.vue b/components/account/AccountHeader.vue index 68e054b0..24013e03 100644 --- a/components/account/AccountHeader.vue +++ b/components/account/AccountHeader.vue @@ -81,6 +81,7 @@ watchEffect(() => { font-bold sm:text-2xl text-xl :content="getDisplayName(account, { rich: true })" :emojis="account.emojis" + :markdown="false" /> diff --git a/components/content/ContentRich.setup.ts b/components/content/ContentRich.setup.ts index 88c1bfdb..497e4add 100644 --- a/components/content/ContentRich.setup.ts +++ b/components/content/ContentRich.setup.ts @@ -5,13 +5,17 @@ defineOptions({ name: 'ContentRich', }) -const props = defineProps<{ +const { content, emojis, markdown = true } = defineProps<{ content: string + markdown?: boolean emojis?: Emoji[] }>() export default () => h( 'span', { class: 'content-rich' }, - contentToVNode(props.content, emojisArrayToObject(props.emojis || [])), + contentToVNode(content, { + emojis: emojisArrayToObject(emojis || []), + markdown, + }), ) diff --git a/composables/content-parse.ts b/composables/content-parse.ts index 8e85c0d2..ad58d8db 100644 --- a/composables/content-parse.ts +++ b/composables/content-parse.ts @@ -12,7 +12,7 @@ function decode(text: string) { * Parse raw HTML form Mastodon server to AST, * with interop of custom emojis and inline Markdown syntax */ -export function parseMastodonHTML(html: string, customEmojis: Record = {}) { +export function parseMastodonHTML(html: string, customEmojis: Record = {}, markdown = true) { let processed = html // custom emojis .replace(/:([\w-]+?):/g, (_, name) => { @@ -21,45 +21,49 @@ export function parseMastodonHTML(html: string, customEmojis: Record` return `:${name}:` }) + + if (markdown) { // handle code blocks - .replace(/>(```|~~~)(\w*)([\s\S]+?)\1/g, (_1, _2, lang, raw) => { - const code = htmlToText(raw) - const classes = lang ? ` class="language-${lang}"` : '' - return `>
${code}
` - }) + processed = processed + .replace(/>(```|~~~)(\w*)([\s\S]+?)\1/g, (_1, _2, lang, raw) => { + const code = htmlToText(raw) + const classes = lang ? ` class="language-${lang}"` : '' + return `>
${code}
` + }) - walkSync(parse(processed), (node) => { - if (node.type !== TEXT_NODE) - return - const replacements = [ - [/\*\*\*(.*?)\*\*\*/g, '$1'], - [/\*\*(.*?)\*\*/g, '$1'], - [/\*(.*?)\*/g, '$1'], - [/~~(.*?)~~/g, '$1'], - [/`([^`]+?)`/g, '$1'], - [/&[^;]+;/g, (val: string) => decode(val)], - ] as any + walkSync(parse(processed), (node) => { + if (node.type !== TEXT_NODE) + return + const replacements = [ + [/\*\*\*(.*?)\*\*\*/g, '$1'], + [/\*\*(.*?)\*\*/g, '$1'], + [/\*(.*?)\*/g, '$1'], + [/~~(.*?)~~/g, '$1'], + [/`([^`]+?)`/g, '$1'], + [/&[^;]+;/g, (val: string) => decode(val)], + ] as any - for (const [re, replacement] of replacements) { - for (const match of node.value.matchAll(re)) { - if (node.loc) { - const start = match.index! + node.loc[0].start - const end = start + match[0].length + node.loc[0].start - processed = processed.slice(0, start) + match[0].replace(re, replacement) + processed.slice(end) - } - else { - processed = processed.replace(match[0], match[0].replace(re, replacement)) + for (const [re, replacement] of replacements) { + for (const match of node.value.matchAll(re)) { + if (node.loc) { + const start = match.index! + node.loc[0].start + const end = start + match[0].length + node.loc[0].start + processed = processed.slice(0, start) + match[0].replace(re, replacement) + processed.slice(end) + } + else { + processed = processed.replace(match[0], match[0].replace(re, replacement)) + } } } - } - }) + }) + } return parse(processed) } -export async function convertMastodonHTML(html: string, customEmojis: Record = {}) { +export function convertMastodonHTML(html: string, customEmojis: Record = {}) { const tree = parseMastodonHTML(html, customEmojis) - return await render(tree) + return render(tree) } export function htmlToText(html: string) { diff --git a/composables/content-render.ts b/composables/content-render.ts index 337a851f..b0a6ef61 100644 --- a/composables/content-render.ts +++ b/composables/content-render.ts @@ -13,9 +13,12 @@ import AccountHoverWrapper from '~/components/account/AccountHoverWrapper.vue' */ export function contentToVNode( content: string, - customEmojis: Record = {}, + { emojis = {}, markdown = true }: { + emojis?: Record + markdown?: boolean + } = {}, ): VNode { - const tree = parseMastodonHTML(content, customEmojis) + const tree = parseMastodonHTML(content, emojis, markdown) return h(Fragment, (tree.children as Node[]).map(n => treeToVNode(n))) } diff --git a/tests/content-rich.test.ts b/tests/content-rich.test.ts index 0ffe9cf1..25256408 100644 --- a/tests/content-rich.test.ts +++ b/tests/content-rich.test.ts @@ -55,7 +55,7 @@ describe('content-rich', () => { }) async function render(content: string, emojis?: Record) { - const vnode = contentToVNode(content, emojis) + const vnode = contentToVNode(content, { emojis }) const html = (await renderToString(vnode)) .replace(//g, '') let formatted = ''