File size: 3,930 Bytes
d3ab1f5
1302517
5c52644
1302517
8cc084b
 
 
c4ae44d
5c52644
1302517
f60772d
 
 
 
 
 
 
5c52644
f60772d
 
 
 
 
 
8cc084b
c4ae44d
 
 
 
 
 
 
 
 
 
 
 
 
8cc084b
 
 
 
 
 
 
 
 
 
 
c4ae44d
adb8afe
 
 
c4ae44d
 
d3ab1f5
 
f60772d
2961a5b
8cc084b
 
 
c4ae44d
 
 
 
 
 
 
 
 
5c52644
 
 
 
 
 
c4ae44d
 
 
 
 
 
 
 
 
 
204c6ba
 
8b92de2
204c6ba
 
 
 
c4ae44d
 
 
2d9278d
c4ae44d
 
 
 
 
 
 
 
 
 
 
 
2961a5b
 
d3ab1f5
5c52644
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe3e1db
5c52644
 
 
 
 
 
 
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
<script lang="ts">
	import Button from '$lib/components/ui/button/button.svelte';
	import { Check, Copy, Eye, X } from '@lucide/svelte';
	import { HighlightAuto } from 'svelte-highlight';
	import { mode } from 'mode-watcher';
	import githubDarkUrl from 'svelte-highlight/styles/github-dark.css?url';
	import githubUrl from 'svelte-highlight/styles/github.css?url';
	import Switch from '$lib/components/ui/switch/switch.svelte';
	import * as Dialog from '$lib/components/ui/dialog/index.js';

	let {
		lang,
		text,
		className = 'my-3 relative rounded-lg border border-border/60 bg-muted/50'
	}: { lang?: string; text: string; className?: string } = $props();

	let copiedCode = $state(false);
	let open = $state.raw(false);

	async function copy(text: string) {
		await navigator.clipboard.writeText(text);
		copiedCode = true;
		setTimeout(() => (copiedCode = false), 2000);
	}

	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);
	}

	$effect(() => {
		const themeUrl = mode.current === 'dark' ? githubDarkUrl : githubUrl;
		let link = document.getElementById('highlight-theme') as HTMLLinkElement | null;
		if (!link) {
			link = document.createElement('link');
			link.id = 'highlight-theme';
			link.rel = 'stylesheet';
			document.head.appendChild(link);
		}
		link.href = themeUrl;
	});

	let canShowPreview = $derived(
		hasStrictHtml5Doctype(text) || isSvgDocument(text) || lang == 'svg' || lang == 'xml'
	);
	// svelte-ignore state_referenced_locally
	let showPreview = $state.raw(canShowPreview);
</script>

<div class="overflow-hidden {className}">
	{#if lang}
		<div
			class="flex items-center justify-between border-b border-border/60 bg-muted px-3 py-1.5 dark:bg-accent/30"
		>
			<span class="font-mono text-xs text-muted-foreground">
				{#if showPreview}
					Live Preview
				{:else}
					{lang}
				{/if}
			</span>
			{#if canShowPreview}
				<div class="flex items-center gap-1.5">
					{#if showPreview}
						<Button variant="outline" size="2xs" onclick={() => (open = true)}>
							<Eye class="size-3.5" />
							Open
						</Button>
					{/if}
					<p class="font-mono text-[10px] text-muted-foreground">
						Show {showPreview ? 'Preview' : 'Code'}
					</p>
					<Switch bind:checked={showPreview} />
				</div>
			{/if}
		</div>
	{/if}
	{#if showPreview}
		<div class="group relative">
			<iframe
				srcDoc={text}
				class="h-[500px] w-full"
				title="Preview"
				sandbox="allow-scripts allow-popups"
				referrerpolicy="no-referrer"
			></iframe>
		</div>
	{:else}
		<div class="group relative">
			<HighlightAuto code={text} class="font-mono text-xs! leading-relaxed" />
			<Button
				variant="outline"
				class="absolute top-2 right-2 opacity-0 group-hover:opacity-100"
				size="icon-xs"
				onclick={() => copy(text)}
			>
				{#if copiedCode}
					<Check class="size-3.5 text-green-500" />
				{:else}
					<Copy class="size-3.5" />
				{/if}
			</Button>
		</div>
	{/if}
</div>

<Dialog.Root bind:open>
	<Dialog.Content
		class="max-w-[90dvw]! gap-0! space-y-0! border-none! p-0!"
		showCloseButton={false}
	>
		<Dialog.Description class="relative mt-0 pt-0!">
			<Button
				variant="outline"
				class="absolute top-2 right-2"
				size="icon-xs"
				onclick={() => (open = false)}
			>
				<X class="size-3.5" />
			</Button>
			<iframe
				srcDoc={text}
				class="h-[90dvh] w-full"
				title="Preview"
				sandbox="allow-scripts allow-popups"
				referrerpolicy="no-referrer"
			></iframe>
		</Dialog.Description>
	</Dialog.Content>
</Dialog.Root>