Adrien Denat commited on
Commit
697285c
1 Parent(s): 5860c9b

refactor syntax highlighting, more performant + avoid flickering on copy to clipboard button (#49)

Browse files
src/lib/components/CodeBlock.svelte ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { afterUpdate } from 'svelte';
3
+ import CopyToClipBoardBtn from './CopyToClipBoardBtn.svelte';
4
+
5
+ export let code = '';
6
+ export let lang = '';
7
+
8
+ $: highlightedCode = '';
9
+
10
+ afterUpdate(async () => {
11
+ const { default: hljs } = await import('highlight.js');
12
+ const language = hljs.getLanguage(lang);
13
+
14
+ highlightedCode = hljs.highlightAuto(code, language?.aliases).value;
15
+ });
16
+ </script>
17
+
18
+ <div class="group code-block">
19
+ <pre><code class="language-{lang}">{@html highlightedCode || code}</code></pre>
20
+ <CopyToClipBoardBtn
21
+ classNames="absolute top-2 right-2 invisible opacity-0 group-hover:visible group-hover:opacity-100"
22
+ value={code}
23
+ />
24
+ </div>
src/lib/components/chat/ChatMessage.svelte CHANGED
@@ -1,86 +1,22 @@
1
  <script lang="ts">
2
  import { marked } from 'marked';
3
  import type { Message } from '$lib/types/Message';
4
- import { afterUpdate } from 'svelte';
5
- import { browser } from '$app/environment';
6
 
7
- import CopyToClipBoardBtn from '../CopyToClipBoardBtn.svelte';
8
 
9
  function sanitizeMd(md: string) {
10
  return md.replaceAll('<', '&lt;');
11
  }
12
 
13
  export let message: Message;
14
- let html = '';
15
  let el: HTMLElement;
16
 
17
- const renderer = new marked.Renderer();
18
-
19
- // Add wrapper to code blocks
20
- renderer.code = (code, lang) => {
21
- return `
22
- <div class="group code-block">
23
- <pre>
24
- <code class="language-${lang}">${code}</code>
25
- </pre>
26
- </div>
27
- `.replaceAll('\t', '');
28
- };
29
-
30
- const handleParsed = (err: Error | null, parsedHtml: string) => {
31
- if (err) {
32
- console.error(err);
33
- } else {
34
- html = parsedHtml;
35
- }
36
- };
37
-
38
  const options: marked.MarkedOptions = {
39
  ...marked.getDefaults(),
40
- gfm: true,
41
- highlight: (code, lang, callback) => {
42
- import('highlight.js').then(
43
- ({ default: hljs }) => {
44
- const language = hljs.getLanguage(lang);
45
- callback?.(null, hljs.highlightAuto(code, language?.aliases).value);
46
- },
47
- (err) => {
48
- console.error(err);
49
- callback?.(err);
50
- }
51
- );
52
- },
53
- renderer
54
  };
55
 
56
- $: browser &&
57
- message.from === 'assistant' &&
58
- marked(sanitizeMd(message.content), options, handleParsed);
59
-
60
- if (message.from === 'assistant') {
61
- html = marked(sanitizeMd(message.content), options);
62
- }
63
-
64
- afterUpdate(() => {
65
- if (el) {
66
- const codeBlocks = el.querySelectorAll('.code-block');
67
-
68
- // Add copy to clipboard button to each code block
69
- codeBlocks.forEach((block) => {
70
- if (block.classList.contains('has-copy-btn')) return;
71
-
72
- new CopyToClipBoardBtn({
73
- target: block,
74
- props: {
75
- value: (block as HTMLElement).innerText ?? '',
76
- classNames:
77
- 'absolute top-2 right-2 invisible opacity-0 group-hover:visible group-hover:opacity-100'
78
- }
79
- });
80
- block.classList.add('has-copy-btn');
81
- });
82
- }
83
- });
84
  </script>
85
 
86
  {#if message.from === 'assistant'}
@@ -94,7 +30,13 @@
94
  class="prose dark:prose-invert :prose-pre:bg-gray-100 dark:prose-pre:bg-gray-950 relative rounded-2xl px-5 py-3.5 border border-gray-100 bg-gradient-to-br from-gray-50 dark:from-gray-800/40 dark:border-gray-800 text-gray-600 dark:text-gray-300"
95
  bind:this={el}
96
  >
97
- {@html html}
 
 
 
 
 
 
98
  </div>
99
  </div>
100
  {/if}
 
1
  <script lang="ts">
2
  import { marked } from 'marked';
3
  import type { Message } from '$lib/types/Message';
 
 
4
 
5
+ import CodeBlock from '../CodeBlock.svelte';
6
 
7
  function sanitizeMd(md: string) {
8
  return md.replaceAll('<', '&lt;');
9
  }
10
 
11
  export let message: Message;
 
12
  let el: HTMLElement;
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  const options: marked.MarkedOptions = {
15
  ...marked.getDefaults(),
16
+ gfm: true
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  };
18
 
19
+ $: tokens = marked.lexer(sanitizeMd(message.content));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  </script>
21
 
22
  {#if message.from === 'assistant'}
 
30
  class="prose dark:prose-invert :prose-pre:bg-gray-100 dark:prose-pre:bg-gray-950 relative rounded-2xl px-5 py-3.5 border border-gray-100 bg-gradient-to-br from-gray-50 dark:from-gray-800/40 dark:border-gray-800 text-gray-600 dark:text-gray-300"
31
  bind:this={el}
32
  >
33
+ {#each tokens as token}
34
+ {#if token.type === 'code'}
35
+ <CodeBlock lang={token.lang} code={token.text} />
36
+ {:else}
37
+ {@html marked.parser([token], options)}
38
+ {/if}
39
+ {/each}
40
  </div>
41
  </div>
42
  {/if}