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>