| <script lang="ts"> |
| import CopyToClipBoardBtn from "./CopyToClipBoardBtn.svelte"; |
| import DOMPurify from "isomorphic-dompurify"; |
| import HtmlPreviewModal from "./HtmlPreviewModal.svelte"; |
| import PlayFilledAlt from "~icons/carbon/play-filled-alt"; |
| import EosIconsLoading from "~icons/eos-icons/loading"; |
| |
| interface Props { |
| code?: string; |
| rawCode?: string; |
| loading?: boolean; |
| } |
| |
| let { code = "", rawCode = "", loading = false }: Props = $props(); |
| |
| let previewOpen = $state(false); |
| |
| function hasStrictHtml5Doctype(input: string): boolean { |
| if (!input) return false; |
| const withoutBOM = input.replace(/^\uFEFF/, ""); |
| const trimmed = withoutBOM.trimStart(); |
| |
| return /^<!doctype\s+html\s*>/i.test(trimmed); |
| } |
| |
| function isSvgDocument(input: string): boolean { |
| const trimmed = input.trimStart(); |
| return /^(?:<\?xml[^>]*>\s*)?(?:<!doctype\s+svg[^>]*>\s*)?<svg[\s>]/i.test(trimmed); |
| } |
| |
| let showPreview = $derived(hasStrictHtml5Doctype(rawCode) || isSvgDocument(rawCode)); |
| </script> |
|
|
| <div class="group relative my-4 rounded-lg"> |
| <div class="pointer-events-none sticky top-0 w-full"> |
| <div |
| class="pointer-events-auto absolute right-2 top-2 flex items-center gap-1.5 md:right-3 md:top-3" |
| > |
| {#if showPreview} |
| <button |
| class="btn h-7 gap-1 rounded-lg border px-2 text-xs shadow-sm backdrop-blur transition-none hover:border-gray-500 active:shadow-inner disabled:cursor-not-allowed disabled:opacity-80 dark:border-gray-600 dark:bg-gray-600/50 dark:hover:border-gray-500" |
| disabled={loading} |
| onclick={() => { |
| if (!loading) { |
| previewOpen = true; |
| } |
| }} |
| title="Preview HTML" |
| aria-label="Preview HTML" |
| > |
| {#if loading} |
| <EosIconsLoading class="size-3.5" /> |
| {:else} |
| <PlayFilledAlt class="size-3.5" /> |
| {/if} |
| Preview |
| </button> |
| {/if} |
| <CopyToClipBoardBtn |
| iconClassNames="size-3" |
| classNames="btn transition-none rounded-lg border size-7 text-sm shadow-sm dark:bg-gray-600/50 backdrop-blur dark:hover:border-gray-500 active:shadow-inner dark:border-gray-600 hover:border-gray-500" |
| value={rawCode} |
| /> |
| </div> |
| </div> |
| <pre class="scrollbar-custom overflow-auto px-5 font-mono transition-[height]"><code |
| >{@html DOMPurify.sanitize(code)}</code |
| ></pre> |
|
|
| {#if previewOpen} |
| <HtmlPreviewModal html={rawCode} onclose={() => (previewOpen = false)} /> |
| {/if} |
| </div> |
|
|