diff options
| author | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
|---|---|---|
| committer | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
| commit | bce557cc2dc767628bed6aac87301a1be7c5431b (patch) | |
| tree | b51a051228d01fe3306cd7626d4a96768aadb944 /src/components/jet/item/LinkableTextItem.svelte | |
init commit
Diffstat (limited to 'src/components/jet/item/LinkableTextItem.svelte')
| -rw-r--r-- | src/components/jet/item/LinkableTextItem.svelte | 88 |
1 files changed, 88 insertions, 0 deletions
diff --git a/src/components/jet/item/LinkableTextItem.svelte b/src/components/jet/item/LinkableTextItem.svelte new file mode 100644 index 0000000..a5a3e74 --- /dev/null +++ b/src/components/jet/item/LinkableTextItem.svelte @@ -0,0 +1,88 @@ +<script lang="ts"> + import type { LinkableText, Action } from '@jet-app/app-store/api/models'; + import { sanitizeHtml } from '@amp/web-app-components/src/utils/sanitize-html'; + import LinkWrapper from '~/components/LinkWrapper.svelte'; + + export let item: LinkableText; + + type Fragment = { + text: string; + action?: Action; + isTrailingPunctuation?: boolean; + }; + + const { + linkedSubstrings = {}, + styledText: { rawText }, + } = item; + + // `LinkableText` items contain a `rawText` string, and an object of `linkedSubstrings`, + // where the key of the object is the substring to replace in the `rawText` and whose value + // is the `Action` that the link should trigger. + // + // That means we have to render replace the keys from `linkedSubstrings` in the `rawText`. + // To do this, we build a regex to match all the strings that are supposed to be linked, + // then build an array of objects representing the fully text, with the `Action` appended + // to the fragments that need to be linked. + const fragmentsToLink = Object.keys(linkedSubstrings); + let fragments: Fragment[]; + + if (fragmentsToLink.length === 0) { + fragments = [{ text: rawText }]; + } else { + // Escapes regex-sensitive characters in the text, so characters like `.` or `+` don't act as regex operators + const cleanedFragmentsToLink = fragmentsToLink.map((fragment) => + fragment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), + ); + + const pattern = new RegExp( + `(${cleanedFragmentsToLink.join('|')})`, + 'g', + ); + + // After we split our text into an array representing the seqence of the raw text, with the + // linkable items as their own entries, we transform the array to contain include the linkable + // items actions, which we then use to determine if we want to render a `LinkWrapper` or plain-text. + fragments = rawText.split(pattern).map((fragment): Fragment => { + const action = linkedSubstrings[fragment]; + + if (action) { + return { action, text: fragment }; + } else { + const isTrailingPunctuation = /^[.,;:!?)\]}"”»']+$/.test( + fragment.trim(), + ); + + return { + isTrailingPunctuation, + text: fragment, + }; + } + }); + } +</script> + +{#each fragments as fragment} + {#if fragment.action} + <LinkWrapper + action={fragment.action} + includeExternalLinkArrowIcon={false} + > + {fragment.text} + </LinkWrapper> + {:else if fragment.isTrailingPunctuation} + <span class="trailing-punctuation">{fragment.text}</span> + {:else} + {@html sanitizeHtml(fragment.text)} + {/if} +{/each} + +<style> + span :global(a:hover) { + text-decoration: underline; + } + + .trailing-punctuation { + margin-inline-start: -0.45ch; + } +</style> |
