1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-02-06 17:07:57 +00:00
Files
pocket-id/frontend/src/lib/components/formatted-message.svelte
Kyle Mendell 484c2f6ef2 feat: user application dashboard (#727)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-08-10 15:56:03 +00:00

101 lines
2.8 KiB
Svelte

<!-- Component to display messages from Paraglide with support for links in the format <link href="url">text</link>. -->
<!-- This gets redundant in the future, because the library will support this natively. https://github.com/opral/inlang-sdk/issues/240 -->
<script lang="ts">
let {
m
}: {
m: string;
} = $props();
interface MessagePart {
type: 'text' | 'link' | 'bold';
content: string;
href?: string;
}
// Extracts attribute value from a tag's attribute string
function getAttr(attrs: string, name: string): string | undefined {
const re = new RegExp(`\\b${name}\\s*=\\s*(["'])(.*?)\\1`, 'i');
const m = re.exec(attrs ?? '');
return m?.[2];
}
const handlers: Record<string, (attrs: string, inner: string) => MessagePart | null> = {
link: (attrs, inner) => {
const href = getAttr(attrs, 'href');
if (!href) return { type: 'text', content: inner };
return { type: 'link', content: inner, href };
},
b: (_attrs, inner) => ({ type: 'bold', content: inner })
};
function buildTokenRegex(): RegExp {
const keys = Object.keys(handlers).join('|');
// Matches: <tag attrs>inner</tag> for allowed tags only
return new RegExp(`<(${keys})\\b([^>]*)>(.*?)<\\/\\1>`, 'g');
}
function parseMessage(content: string): MessagePart[] | string {
const tokenRegex = buildTokenRegex();
if (!tokenRegex.test(content)) return content;
// Reset lastIndex for reuse
tokenRegex.lastIndex = 0;
const parts: MessagePart[] = [];
let lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = tokenRegex.exec(content)) !== null) {
// Add text before the matched token
if (match.index > lastIndex) {
const textContent = content.slice(lastIndex, match.index);
if (textContent) parts.push({ type: 'text', content: textContent });
}
const tag = match[1];
const attrs = match[2] ?? '';
const inner = match[3] ?? '';
const handler = handlers[tag];
const part: MessagePart | null = handler
? handler(attrs, inner)
: { type: 'text', content: inner };
if (part) parts.push(part);
lastIndex = match.index + match[0].length;
}
// Add remaining text after the last token
if (lastIndex < content.length) {
const remainingText = content.slice(lastIndex);
if (remainingText) parts.push({ type: 'text', content: remainingText });
}
return parts;
}
const parsedContent = parseMessage(m);
</script>
{#if typeof parsedContent === 'string'}
{parsedContent}
{:else}
{#each parsedContent as part}
{#if part.type === 'text'}
{part.content}
{:else if part.type === 'bold'}
<b>
{part.content}
</b>
{:else if part.type === 'link'}
<a
class="text-black underline dark:text-white"
href={part.href}
target="_blank"
rel="noopener noreferrer"
>
{part.content}
</a>
{/if}
{/each}
{/if}