Files
beavergram/frontend/src/lib/components/EntityText.svelte
T

210 lines
4.4 KiB
Svelte

<script lang="ts">
import { SvelteSet } from "svelte/reactivity";
import type { EntityView } from "#lib/api/types.js";
import {
buildEntityTree,
type EntityNode,
type EntityTreeNode,
jumboEmojiCount,
linkHref,
} from "#lib/format/entities.js";
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>