199 lines
4.4 KiB
Svelte
199 lines
4.4 KiB
Svelte
<script lang="ts">
|
|
import { SvelteSet } from "svelte/reactivity";
|
|
import type { EntityView } from "$lib/api/types";
|
|
import {
|
|
buildEntityTree,
|
|
type EntityNode,
|
|
type EntityTreeNode,
|
|
jumboEmojiCount,
|
|
linkHref,
|
|
} from "$lib/format/entities";
|
|
|
|
interface Props {
|
|
entities: EntityView[];
|
|
own?: boolean;
|
|
text: string;
|
|
}
|
|
|
|
let { text, entities, own = false }: Props = $props();
|
|
|
|
const nodes = $derived(buildEntityTree(text, entities));
|
|
const jumbo = $derived(entities.length === 0 ? jumboEmojiCount(text) : 0);
|
|
|
|
const revealed = new SvelteSet<string>();
|
|
|
|
function spoilerKey(entity: EntityView): string {
|
|
return `${entity.offset}:${entity.length}`;
|
|
}
|
|
</script>
|
|
|
|
{#snippet tree(items: EntityTreeNode[])}
|
|
{#each items as node, i (i)}
|
|
{#if node.kind === "text"}
|
|
{node.text}
|
|
{:else}
|
|
{@render entity(node)}
|
|
{/if}
|
|
{/each}
|
|
{/snippet}
|
|
|
|
{#snippet entity(node: EntityNode)}
|
|
{@const type = node.entity.type}
|
|
{#if type === "bold"}
|
|
<strong>{@render tree(node.children)}</strong>
|
|
{:else if type === "italic"}
|
|
<em>{@render tree(node.children)}</em>
|
|
{:else if type === "underline"}
|
|
<ins>{@render tree(node.children)}</ins>
|
|
{:else if type === "strikethrough"}
|
|
<del>{@render tree(node.children)}</del>
|
|
{:else if type === "spoiler"}
|
|
{@const key = spoilerKey(node.entity)}
|
|
<button
|
|
class="spoiler"
|
|
onclick={() => revealed.add(key)}
|
|
type="button"
|
|
class:revealed={revealed.has(key)}
|
|
>
|
|
{@render tree(node.children)}
|
|
</button>
|
|
{:else if type === "code"}
|
|
<code class="code" class:own>{@render tree(node.children)}</code>
|
|
{:else if type === "pre"}
|
|
<pre
|
|
class="pre"
|
|
class:own
|
|
>{#if node.entity.language}<span class="pre-lang">{node.entity.language}</span>{/if}<code>{@render tree(node.children)}</code></pre>
|
|
{:else if type === "blockquote"}
|
|
<blockquote class="blockquote">{@render tree(node.children)}</blockquote>
|
|
{:else if type === "url" || type === "text_link" || type === "email" || type === "phone_number"}
|
|
<a
|
|
class="link"
|
|
href={linkHref(node)}
|
|
rel="noopener noreferrer"
|
|
target="_blank"
|
|
class:own
|
|
>{@render tree(node.children)}</a
|
|
>
|
|
{:else if type === "mention" || type === "text_mention" || type === "hashtag" || type === "cashtag" || type === "bot_command"}
|
|
<span class="link" class:own>{@render tree(node.children)}</span>
|
|
{:else}
|
|
{@render tree(node.children)}
|
|
{/if}
|
|
{/snippet}
|
|
|
|
<span class="EntityText" class:jumbo={jumbo > 0} class:jumbo-1={jumbo === 1}>
|
|
{@render tree(nodes)}
|
|
</span>
|
|
|
|
<style lang="scss">
|
|
.EntityText {
|
|
overflow-wrap: anywhere;
|
|
white-space: pre-wrap;
|
|
|
|
&.jumbo {
|
|
font-size: 1.75rem;
|
|
line-height: 1.25;
|
|
}
|
|
|
|
&.jumbo-1 {
|
|
font-size: 2.5rem;
|
|
}
|
|
}
|
|
|
|
.link {
|
|
color: var(--color-links);
|
|
text-decoration: none;
|
|
word-break: break-all;
|
|
|
|
&:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
&.own {
|
|
color: var(--color-own-links);
|
|
}
|
|
}
|
|
|
|
a.link.own {
|
|
text-decoration: underline;
|
|
text-decoration-color: color-mix(
|
|
in srgb,
|
|
var(--color-own-links) 45%,
|
|
transparent
|
|
);
|
|
text-underline-offset: 0.15em;
|
|
|
|
&:hover {
|
|
text-decoration-color: currentcolor;
|
|
}
|
|
}
|
|
|
|
.code,
|
|
.pre {
|
|
font-family: var(--font-family-monospace, monospace);
|
|
font-size: 0.9375rem;
|
|
color: var(--color-code);
|
|
background-color: var(--color-code-bg);
|
|
|
|
&.own {
|
|
color: var(--color-code-own);
|
|
background-color: var(--color-code-own-bg);
|
|
}
|
|
}
|
|
|
|
.code {
|
|
padding: 0.0625rem 0.25rem;
|
|
border-radius: 0.25rem;
|
|
}
|
|
|
|
.pre {
|
|
overflow-x: auto;
|
|
|
|
display: block;
|
|
margin: 0.125rem 0;
|
|
padding: 0.375rem 0.5rem;
|
|
border-radius: 0.375rem;
|
|
|
|
white-space: pre;
|
|
}
|
|
|
|
.pre-lang {
|
|
display: block;
|
|
margin-bottom: 0.25rem;
|
|
|
|
font-size: 0.75rem;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.blockquote {
|
|
margin: 0.125rem 0;
|
|
padding: 0.125rem 0.5rem;
|
|
border-left: 0.1875rem solid var(--color-links);
|
|
border-radius: 0.25rem;
|
|
|
|
background-color: var(--color-primary-tint);
|
|
}
|
|
|
|
.spoiler {
|
|
cursor: pointer;
|
|
|
|
padding: 0;
|
|
border: none;
|
|
|
|
font: inherit;
|
|
color: transparent;
|
|
text-shadow: none;
|
|
|
|
background-color: var(--color-text);
|
|
border-radius: 0.25rem;
|
|
|
|
transition: color 0.15s, background-color 0.15s;
|
|
|
|
&.revealed {
|
|
color: inherit;
|
|
background-color: transparent;
|
|
}
|
|
}
|
|
</style>
|