|
37 | 37 | import bash from "svelte-highlight/languages/bash"; |
38 | 38 | import sql from "svelte-highlight/languages/sql"; |
39 | 39 | import { marked } from "marked"; |
40 | | - export let label = ""; |
| 40 | + import { afterUpdate, onMount } from "svelte"; |
| 41 | +
|
41 | 42 | export let output = ""; |
42 | | - export let languages = "Python"; |
| 43 | + export let lang = "Python"; |
43 | 44 | export let isCode = false; |
| 45 | + export let md_output = ""; |
| 46 | + export let segments: Segment[] = []; |
44 | 47 |
|
| 48 | + let outputEl: HTMLDivElement; |
45 | 49 | let copyText = "copy"; |
| 50 | + let shouldAutoscroll = true; |
| 51 | +
|
| 52 | + type Segment = { |
| 53 | + id: number; |
| 54 | + type: "text" | "code"; |
| 55 | + content: string; |
| 56 | + lang?: string; |
| 57 | + }; |
46 | 58 |
|
47 | 59 | const languagesTag = { |
48 | 60 | Typescript: typescript, |
|
65 | 77 | Lua: lua, |
66 | 78 | Bash: bash, |
67 | 79 | Sql: sql, |
68 | | - } as { [key: string]: any }; |
69 | | -
|
70 | | - function copyToClipboard(text) { |
71 | | - const textArea = document.createElement("textarea"); |
72 | | - textArea.value = text; |
73 | | - document.body.appendChild(textArea); |
74 | | - textArea.select(); |
75 | | - document.execCommand("copy"); |
76 | | - document.body.removeChild(textArea); |
| 80 | + } as const; |
| 81 | +
|
| 82 | + type LangKey = keyof typeof languagesTag; |
| 83 | +
|
| 84 | + const aliasMap: Record<string, LangKey> = { |
| 85 | + javascript: "Javascript", |
| 86 | + js: "Javascript", |
| 87 | + jsx: "Javascript", |
| 88 | + typescript: "Typescript", |
| 89 | + ts: "Typescript", |
| 90 | + tsx: "Typescript", |
| 91 | +
|
| 92 | + python: "Python", |
| 93 | + py: "Python", |
| 94 | +
|
| 95 | + c: "C", |
| 96 | + "c++": "Cpp", |
| 97 | + cpp: "Cpp", |
| 98 | + cxx: "Cpp", |
| 99 | + csharp: "Csharp", |
| 100 | + "c#": "Csharp", |
| 101 | +
|
| 102 | + go: "Go", |
| 103 | + golang: "Go", |
| 104 | + java: "Java", |
| 105 | + swift: "Swift", |
| 106 | + ruby: "Ruby", |
| 107 | + rust: "Rust", |
| 108 | + php: "Php", |
| 109 | + kotlin: "Kotlin", |
| 110 | + objectivec: "Objectivec", |
| 111 | + objc: "Objectivec", |
| 112 | + "objective-c": "Objectivec", |
| 113 | + perl: "Perl", |
| 114 | + matlab: "Matlab", |
| 115 | + r: "R", |
| 116 | + lua: "Lua", |
| 117 | +
|
| 118 | + bash: "Bash", |
| 119 | + sh: "Bash", |
| 120 | + shell: "Bash", |
| 121 | + zsh: "Bash", |
| 122 | +
|
| 123 | + sql: "Sql", |
| 124 | + }; |
| 125 | +
|
| 126 | + $: normalizedLangKey = (() => { |
| 127 | + const raw = (lang ?? "").toString().trim(); |
| 128 | + if (!raw) return null; |
| 129 | + const lower = raw.toLowerCase(); |
| 130 | +
|
| 131 | + if (lower in aliasMap) return aliasMap[lower]; |
| 132 | +
|
| 133 | + const hit = (Object.keys(languagesTag) as LangKey[]).find( |
| 134 | + (k) => k.toLowerCase() === lower |
| 135 | + ); |
| 136 | + return hit ?? null; |
| 137 | + })(); |
| 138 | +
|
| 139 | + $: fullText = buildFullText(); |
| 140 | +
|
| 141 | + function atBottom(el: HTMLElement, threshold = 8) { |
| 142 | + return el.scrollHeight - el.scrollTop - el.clientHeight <= threshold; |
| 143 | + } |
| 144 | +
|
| 145 | + function handleScroll() { |
| 146 | + if (!outputEl) return; |
| 147 | + shouldAutoscroll = atBottom(outputEl); |
77 | 148 | } |
78 | 149 |
|
79 | | - function handelCopy() { |
80 | | - copyToClipboard(output); |
| 150 | + function scrollToBottom() { |
| 151 | + if (!outputEl) return; |
| 152 | + requestAnimationFrame(() => |
| 153 | + requestAnimationFrame(() => { |
| 154 | + if (outputEl.scrollHeight) { |
| 155 | + outputEl.scrollTop = outputEl.scrollHeight; |
| 156 | + } |
| 157 | + }) |
| 158 | + ); |
| 159 | + } |
| 160 | +
|
| 161 | + onMount(() => { |
| 162 | + scrollToBottom(); |
| 163 | + }); |
| 164 | +
|
| 165 | + afterUpdate(() => { |
| 166 | + if (shouldAutoscroll) scrollToBottom(); |
| 167 | + }); |
| 168 | + async function copyAllFromDiv() { |
| 169 | + await navigator.clipboard.writeText(outputEl.innerText); |
81 | 170 | copyText = "copied!"; |
82 | | - setTimeout(() => { |
83 | | - copyText = "copy"; |
84 | | - }, 1000); |
| 171 | + setTimeout(() => (copyText = "copy"), 1000); |
| 172 | + } |
| 173 | +
|
| 174 | + function copyToClipboard(text: string) { |
| 175 | + if (navigator?.clipboard?.writeText) { |
| 176 | + navigator.clipboard.writeText(text); |
| 177 | + } else { |
| 178 | + const textArea = document.createElement("textarea"); |
| 179 | + textArea.value = text; |
| 180 | + document.body.appendChild(textArea); |
| 181 | + textArea.select(); |
| 182 | + document.execCommand("copy"); |
| 183 | + document.body.removeChild(textArea); |
| 184 | + } |
| 185 | + } |
| 186 | +
|
| 187 | + function normalizeToKey(raw?: string | null) { |
| 188 | + const s = (raw ?? "").trim().toLowerCase(); |
| 189 | + if (!s) return null; |
| 190 | + if (s in aliasMap) return aliasMap[s as keyof typeof aliasMap]; |
| 191 | + const hit = ( |
| 192 | + Object.keys(languagesTag) as (keyof typeof languagesTag)[] |
| 193 | + ).find((k) => k.toLowerCase() === s); |
| 194 | + return hit ?? null; |
| 195 | + } |
| 196 | +
|
| 197 | + function buildFullText(): string { |
| 198 | + if (segments && segments.length > 0) { |
| 199 | + return segments |
| 200 | + .map((seg) => { |
| 201 | + if (seg.type === "code") { |
| 202 | + const key = normalizeToKey(seg.lang) ?? "text"; |
| 203 | + return ["```" + key.toLowerCase(), seg.content, "```"].join("\n"); |
| 204 | + } |
| 205 | + return seg.content; |
| 206 | + }) |
| 207 | + .join("\n\n"); |
| 208 | + } |
| 209 | +
|
| 210 | + const parts: string[] = []; |
| 211 | + if (isCode && output) { |
| 212 | + const key = (normalizedLangKey ?? "text").toLowerCase(); |
| 213 | + parts.push(["```" + key, output, "```"].join("\n")); |
| 214 | + } |
| 215 | + if (md_output) { |
| 216 | + parts.push(md_output); |
| 217 | + } |
| 218 | + return parts.join("\n\n"); |
85 | 219 | } |
86 | 220 | </script> |
87 | 221 |
|
88 | 222 | <div class="flex w-full flex-col" data-testid="code-output"> |
89 | | - <span |
90 | | - class=" mb-2 flex h-[3rem] w-full items-center justify-center bg-[#5856D6] px-8 py-2 text-center text-[0.89rem] uppercase leading-tight opacity-80" |
91 | | - >{label}</span |
92 | | - > |
93 | | - |
94 | 223 | <div |
95 | 224 | class="flex justify-end border-2 border-none border-b-gray-800 bg-[#1C1C1C] px-3 text-white" |
96 | 225 | > |
97 | 226 | <button |
98 | 227 | class="rounded border border-none py-1 text-[0.8rem] text-[#abb2bf]" |
99 | | - on:click={() => { |
100 | | - handelCopy(); |
101 | | - }}>{copyText}</button |
| 228 | + on:click={copyAllFromDiv}>{copyText}</button |
102 | 229 | > |
103 | 230 | </div> |
| 231 | + |
104 | 232 | <div |
105 | | - class="code-format-style hiddenScroll h-[22rem] divide-y overflow-auto bg-[#011627]" |
| 233 | + class=" |
| 234 | + hiddenScroll h-[22rem] overflow-auto |
| 235 | + bg-[#011627] p-5 text-[13px] |
| 236 | + leading-5 |
| 237 | + " |
| 238 | + bind:this={outputEl} |
| 239 | + on:scroll={handleScroll} |
106 | 240 | > |
107 | | - {#if isCode} |
108 | | - <Highlight language={python} code={output} let:highlighted> |
109 | | - <LineNumbers {highlighted} wrapLines hideBorder /> |
110 | | - </Highlight> |
| 241 | + {#if segments && segments.length > 0} |
| 242 | + {#each segments as seg (seg.id)} |
| 243 | + {#if seg.type === "code"} |
| 244 | + <div class="relative border-t border-[#0c2233]"> |
| 245 | + <Highlight |
| 246 | + language={languagesTag[normalizeToKey(seg.lang) ?? "Python"]} |
| 247 | + code={seg.content} |
| 248 | + let:highlighted |
| 249 | + > |
| 250 | + <LineNumbers {highlighted} wrapLines hideBorder /> |
| 251 | + </Highlight> |
| 252 | + </div> |
| 253 | + {:else} |
| 254 | + <div>{@html marked(seg.content)}</div> |
| 255 | + {/if} |
| 256 | + {/each} |
111 | 257 | {:else} |
112 | | - <div class="bg-[#282c34] text-[#abb2bf]"> |
113 | | - {@html marked(output)} |
114 | | - </div> |
| 258 | + {#if isCode && output} |
| 259 | + <Highlight language={python} code={output} let:highlighted> |
| 260 | + <LineNumbers {highlighted} wrapLines hideBorder /> |
| 261 | + </Highlight> |
| 262 | + {/if} |
| 263 | + {#if md_output} |
| 264 | + <div class="bg-[#282c34] py-2 text-[#abb2bf]"> |
| 265 | + {@html marked(md_output)} |
| 266 | + </div> |
| 267 | + {/if} |
115 | 268 | {/if} |
116 | 269 | </div> |
117 | 270 | </div> |
|
120 | 273 | .hiddenScroll::-webkit-scrollbar { |
121 | 274 | display: none; |
122 | 275 | } |
123 | | -
|
124 | 276 | .hiddenScroll { |
125 | 277 | -ms-overflow-style: none; /* IE and Edge */ |
126 | 278 | scrollbar-width: none; /* Firefox */ |
127 | 279 | } |
128 | | -
|
129 | | - .code-format-style { |
130 | | - resize: none; |
131 | | - font-size: 16px; |
132 | | - border: solid rgba(128, 0, 128, 0) 4px; |
133 | | - box-shadow: 0 0 8px rgba(0, 0, 0, 0.19); |
134 | | - transition: 0.1s linear; |
135 | | - } |
136 | 280 | </style> |
0 commit comments