Spaces:
Runtime error
Runtime error
File size: 2,913 Bytes
d852bfd 98051f8 7bf1507 6e2a902 d852bfd a1a6daf 10c62e9 7bf1507 a1a6daf d852bfd 7bf1507 6e2a902 7bf1507 6e2a902 d852bfd 6a0861b 9d03b5a 7bf1507 5a9c360 7bf1507 5a9c360 7bf1507 5a9c360 6e2a902 bbf8562 7bf1507 d852bfd |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
<script lang="ts">
import CopyToClipBoardBtn from "./CopyToClipBoardBtn.svelte";
import HtmlPreviewModal from "./HtmlPreviewModal.svelte";
import PlayFilledAlt from "~icons/carbon/play-filled-alt";
import EosIconsLoading from "~icons/eos-icons/loading";
import { browser } from "$app/environment";
import { onMount } from "svelte";
interface Props {
code?: string;
rawCode?: string;
loading?: boolean;
}
let { code = "", rawCode = "", loading = false }: Props = $props();
let DOMPurify: typeof import("isomorphic-dompurify").default | null = null;
let sanitizedCode = $state(code);
let previewOpen = $state(false);
function hasStrictHtml5Doctype(input: string): boolean {
if (!input) return false;
const withoutBOM = input.replace(/^\uFEFF/, "");
const trimmed = withoutBOM.trimStart();
// Strict HTML5 doctype: <!doctype html> with optional whitespace before >
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));
onMount(async () => {
if (browser) {
const { default: purify } = await import("isomorphic-dompurify");
DOMPurify = purify;
}
});
$effect(() => {
if (DOMPurify) {
sanitizedCode = DOMPurify.sanitize(code);
} else {
sanitizedCode = code;
}
});
</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-60 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
><!-- eslint-disable svelte/no-at-html-tags -->{@html sanitizedCode}</code
></pre>
{#if previewOpen}
<HtmlPreviewModal html={rawCode} onclose={() => (previewOpen = false)} />
{/if}
</div>
|