MarkdownTokens.svelte 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. <script lang="ts">
  2. import { marked } from 'marked';
  3. import type { Token } from 'marked';
  4. import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
  5. import { onMount } from 'svelte';
  6. import Image from '$lib/components/common/Image.svelte';
  7. import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
  8. import MarkdownInlineTokens from '$lib/components/chat/Messages/MarkdownInlineTokens.svelte';
  9. export let id: string;
  10. export let tokens: Token[];
  11. export let top = true;
  12. let containerElement;
  13. const headerComponent = (depth: number) => {
  14. return 'h' + depth;
  15. };
  16. const renderer = new marked.Renderer();
  17. // For code blocks with simple backticks
  18. renderer.codespan = (code) => {
  19. return `<code class="codespan">${code.replaceAll('&amp;', '&')}</code>`;
  20. };
  21. let codes = [];
  22. renderer.code = (code, lang) => {
  23. codes.push({
  24. code: code,
  25. lang: lang
  26. });
  27. codes = codes;
  28. const codeId = `${id}-${codes.length}`;
  29. const interval = setInterval(() => {
  30. if (document.getElementById(`code-${codeId}`)) {
  31. clearInterval(interval);
  32. new CodeBlock({
  33. target: document.getElementById(`code-${codeId}`),
  34. props: {
  35. id: `${id}-${codes.length}`,
  36. lang: lang,
  37. code: revertSanitizedResponseContent(code)
  38. },
  39. hydrate: true,
  40. $$inline: true
  41. });
  42. }
  43. }, 10);
  44. return `<div id="code-${id}-${codes.length}" />`;
  45. };
  46. let images = [];
  47. renderer.image = (href, title, text) => {
  48. images.push({
  49. href: href,
  50. title: title,
  51. text: text
  52. });
  53. images = images;
  54. const imageId = `${id}-${images.length}`;
  55. const interval = setInterval(() => {
  56. if (document.getElementById(`image-${imageId}`)) {
  57. clearInterval(interval);
  58. new Image({
  59. target: document.getElementById(`image-${imageId}`),
  60. props: {
  61. src: href,
  62. alt: text
  63. },
  64. hydrate: true
  65. });
  66. }
  67. }, 10);
  68. return `<div id="image-${id}-${images.length}" />`;
  69. };
  70. // Open all links in a new tab/window (from https://github.com/markedjs/marked/issues/655#issuecomment-383226346)
  71. const origLinkRenderer = renderer.link;
  72. renderer.link = (href, title, text) => {
  73. const html = origLinkRenderer.call(renderer, href, title, text);
  74. return html.replace(/^<a /, '<a target="_blank" rel="nofollow" ');
  75. };
  76. const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & {
  77. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  78. extensions: any;
  79. };
  80. $: if (tokens) {
  81. images = [];
  82. codes = [];
  83. }
  84. </script>
  85. <div bind:this={containerElement} class="flex flex-col">
  86. {#each tokens as token, tokenIdx (`${id}-${tokenIdx}`)}
  87. {#if token.type === 'code'}
  88. {#if token.lang === 'mermaid'}
  89. <pre class="mermaid">{revertSanitizedResponseContent(token.text)}</pre>
  90. {:else}
  91. <CodeBlock
  92. id={`${id}-${tokenIdx}`}
  93. lang={token?.lang ?? ''}
  94. code={revertSanitizedResponseContent(token?.text ?? '')}
  95. />
  96. {/if}
  97. <!-- {:else if token.type === 'heading'}
  98. <svelte:element this={headerComponent(token.depth)}>
  99. <MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} />
  100. </svelte:element>
  101. {:else if token.type === 'hr'}
  102. <hr />
  103. {:else if token.type === 'blockquote'}
  104. <blockquote>
  105. <svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} />
  106. </blockquote>
  107. {:else if token.type === 'html'}
  108. {@html token.text}
  109. {:else if token.type === 'paragraph'}
  110. <p>
  111. <MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
  112. </p>
  113. {:else if token.type === 'list'}
  114. {#if token.ordered}
  115. <ol start={token.start || 1}>
  116. {#each token.items as item, itemIdx}
  117. <li>
  118. <svelte:self
  119. id={`${id}-${tokenIdx}-${itemIdx}`}
  120. tokens={item.tokens}
  121. top={token.loose}
  122. />
  123. </li>
  124. {/each}
  125. </ol>
  126. {:else}
  127. <ul>
  128. {#each token.items as item, itemIdx}
  129. <li>
  130. <svelte:self
  131. id={`${id}-${tokenIdx}-${itemIdx}`}
  132. tokens={item.tokens}
  133. top={token.loose}
  134. />
  135. </li>
  136. {/each}
  137. </ul>
  138. {/if}
  139. {:else if token.type === 'table'}
  140. <table>
  141. <thead>
  142. <tr>
  143. {#each token.header as header, headerIdx}
  144. <th style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}>
  145. <MarkdownInlineTokens
  146. id={`${id}-${tokenIdx}-header-${headerIdx}`}
  147. tokens={header.tokens}
  148. />
  149. </th>
  150. {/each}
  151. </tr>
  152. </thead>
  153. <tbody>
  154. {#each token.rows as row, rowIdx}
  155. <tr>
  156. {#each row ?? [] as cell, cellIdx}
  157. <td style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}>
  158. <MarkdownInlineTokens
  159. id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
  160. tokens={cell.tokens}
  161. />
  162. </td>
  163. {/each}
  164. </tr>
  165. {/each}
  166. </tbody>
  167. </table>
  168. {:else if token.type === 'text'} -->
  169. <!-- {#if top}
  170. <p>
  171. {#if token.tokens}
  172. <MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} />
  173. {:else}
  174. {unescapeHtml(token.text)}
  175. {/if}
  176. </p>
  177. {:else if token.tokens}
  178. <MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
  179. {:else}
  180. {unescapeHtml(token.text)}
  181. {/if} -->
  182. {:else}
  183. {@html marked.parse(token.raw, {
  184. ...defaults,
  185. gfm: true,
  186. breaks: true,
  187. renderer
  188. })}
  189. {/if}
  190. {/each}
  191. </div>