svelte-streamdown 3.1.2 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -43
- package/dist/Block.svelte +5 -2
- package/dist/Elements/Citation.svelte +2 -2
- package/dist/Elements/Code.svelte +21 -54
- package/dist/Elements/Code.svelte.d.ts +2 -2
- package/dist/Elements/Element.svelte +8 -5
- package/dist/Elements/Link.svelte +2 -1
- package/dist/Elements/Mermaid.svelte +8 -3
- package/dist/Elements/Mermaid.svelte.d.ts +2 -2
- package/dist/Elements/MermaidDownload.svelte +1 -1
- package/dist/Elements/TableDownload.svelte +13 -5
- package/dist/Elements/fallbacks/CodeFallback.svelte +7 -1
- package/dist/Elements/fallbacks/MermaidFallback.svelte +7 -1
- package/dist/Streamdown.svelte +12 -15
- package/dist/context.svelte.d.ts +9 -8
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/marked/index.d.ts +20 -1
- package/dist/marked/index.js +160 -39
- package/dist/marked/marked-br.js +6 -0
- package/dist/marked/marked-citations.js +6 -0
- package/dist/marked/marked-footnotes.js +18 -4
- package/dist/marked/marked-math.js +5 -0
- package/dist/marked/marked-subsup.js +24 -3
- package/dist/marked/marked-table.js +15 -12
- package/dist/theme.d.ts +0 -3
- package/dist/theme.js +0 -2
- package/dist/utils/highlightThemes.d.ts +4 -0
- package/dist/utils/highlightThemes.js +14 -0
- package/dist/utils/hightlighter.svelte.d.ts +6 -29
- package/dist/utils/hightlighter.svelte.js +27 -212
- package/dist/utils/parse-incomplete-markdown.js +100 -18
- package/dist/utils/save.js +4 -1
- package/dist/utils/url.js +6 -2
- package/package.json +5 -6
- package/dist/utils/bundledLanguages.d.ts +0 -8
- package/dist/utils/bundledLanguages.js +0 -143
package/README.md
CHANGED
|
@@ -75,9 +75,9 @@ Full support for
|
|
|
75
75
|
|
|
76
76
|
### 💻 Interactive Code Blocks
|
|
77
77
|
|
|
78
|
-
- Syntax highlighting powered by
|
|
78
|
+
- Syntax highlighting powered by [@tanstack/highlight](https://github.com/TanStack/highlight) (synchronous, SSR-friendly, ~31KB min / ~11KB gzip for every language)
|
|
79
79
|
- Copy-to-clipboard functionality
|
|
80
|
-
- Support any
|
|
80
|
+
- Support any `@tanstack/highlight` theme, or your own
|
|
81
81
|
|
|
82
82
|
### 🔢 Mathematical Expressions
|
|
83
83
|
|
|
@@ -398,7 +398,7 @@ Here is how it works on each content update:
|
|
|
398
398
|
|
|
399
399
|
During streaming, newly received text almost always only changes the **last** block (and occasionally starts a new one). Every earlier block keeps an identical raw string, so Svelte skips its `lex()` call entirely — this is the equivalent of the memoized `Block` component in the React version. The block-splitting pass itself runs on every update, but it is the cheap pass; the costly inline parsing is what gets reused.
|
|
400
400
|
|
|
401
|
-
Code highlighting is incremental as well: a code block is only re-highlighted when its text changes
|
|
401
|
+
Code highlighting is incremental as well: a code block is only re-highlighted when its text changes. Highlighting itself is synchronous and runs during SSR, so there is no grammar to load, no loading state and no hydration flash.
|
|
402
402
|
|
|
403
403
|
> [!NOTE]
|
|
404
404
|
> There is intentionally no separate block-level parse cache (e.g. an LRU keyed by block content). For the common append-only streaming case the reactivity-based approach above already avoids redundant work, and a standalone cache would add memory usage and invalidation complexity without a measurable benefit. If you have a workload where this matters, please [open an issue](https://github.com/beynar/svelte-streamdown/issues) with a repro — we're happy to revisit.
|
|
@@ -512,7 +512,7 @@ Prefixes can also be **protocol-only**, which allows any URL using that protocol
|
|
|
512
512
|
```
|
|
513
513
|
|
|
514
514
|
> [!NOTE]
|
|
515
|
-
> `'*'` allows
|
|
515
|
+
> `'*'` allows every `http:`, `https:`, `mailto:` and `tel:` URL — the protocols a document can legitimately link to. `javascript:`, `data:` and `vbscript:` stay blocked under the wildcard because they execute in the page's origin. A protocol-only prefix only allows that exact protocol, so list each one you want to permit. Only add a protocol you trust — e.g. do not add `'javascript:'`.
|
|
516
516
|
|
|
517
517
|
## 📦 Bundle Optimization
|
|
518
518
|
|
|
@@ -524,7 +524,7 @@ Streamdown is optimized for minimal bundle size by making heavy components **opt
|
|
|
524
524
|
<script>
|
|
525
525
|
import { Streamdown } from 'svelte-streamdown';
|
|
526
526
|
// Import only the components you need
|
|
527
|
-
import Code from 'svelte-streamdown/code'; //
|
|
527
|
+
import Code from 'svelte-streamdown/code'; // syntax highlighting
|
|
528
528
|
import Mermaid from 'svelte-streamdown/mermaid'; // Mermaid diagrams
|
|
529
529
|
import Math from 'svelte-streamdown/math'; // KaTeX math rendering
|
|
530
530
|
</script>
|
|
@@ -534,11 +534,11 @@ Streamdown is optimized for minimal bundle size by making heavy components **opt
|
|
|
534
534
|
|
|
535
535
|
### Component Dependencies
|
|
536
536
|
|
|
537
|
-
| Component | Import Path | Dependency
|
|
538
|
-
| --------- | --------------------------- |
|
|
539
|
-
| `Code` | `svelte-streamdown/code` |
|
|
540
|
-
| `Mermaid` | `svelte-streamdown/mermaid` | Mermaid.js
|
|
541
|
-
| `Math` | `svelte-streamdown/math` | KaTeX
|
|
537
|
+
| Component | Import Path | Dependency | Size Impact |
|
|
538
|
+
| --------- | --------------------------- | --------------------- | ---------------------------------------------------- |
|
|
539
|
+
| `Code` | `svelte-streamdown/code` | `@tanstack/highlight` | ~31KB min / ~11KB gzip (all 30 languages + 2 themes) |
|
|
540
|
+
| `Mermaid` | `svelte-streamdown/mermaid` | Mermaid.js | ~1.5MB |
|
|
541
|
+
| `Math` | `svelte-streamdown/math` | KaTeX | ~300KB |
|
|
542
542
|
|
|
543
543
|
> [!TIP]
|
|
544
544
|
> Only import the components your application actually uses. If your content doesn't include code blocks, mermaid diagrams, or math expressions, you can skip those imports entirely for a much smaller bundle.
|
|
@@ -551,71 +551,150 @@ When a heavy component is not provided:
|
|
|
551
551
|
- **Mermaid**: Renders the mermaid source as a code block
|
|
552
552
|
- **Math**: Renders the raw LaTeX/math text
|
|
553
553
|
|
|
554
|
-
###
|
|
554
|
+
### Highlight themes
|
|
555
555
|
|
|
556
|
-
The `Code` component bundles two themes out of the box: **`github-dark`** and **`github-light`**. By default `
|
|
556
|
+
The `Code` component bundles two themes out of the box: **`github-dark`** and **`github-light`**. By default `highlightTheme` follows the active color scheme (`github-dark` in dark mode, `github-light` otherwise), so basic light/dark theming works with no extra configuration.
|
|
557
557
|
|
|
558
|
-
To use any other
|
|
558
|
+
To use any other theme (`aurora-x`, `dracula`, `gruvbox-dark`, `gruvbox-light`, `monokai`, `nord`, `one-dark-pro`, `solarized-dark`, `solarized-light`), import it from `@tanstack/highlight/themes/<name>` and register it via the `highlightThemes` prop. The **key** you register it under is the value you pass to `highlightTheme`:
|
|
559
559
|
|
|
560
560
|
```svelte
|
|
561
561
|
<script lang="ts">
|
|
562
562
|
import { Streamdown } from 'svelte-streamdown';
|
|
563
|
-
import Code from 'svelte-streamdown/code'; // enables
|
|
564
|
-
import
|
|
563
|
+
import Code from 'svelte-streamdown/code'; // enables highlighting
|
|
564
|
+
import dracula from '@tanstack/highlight/themes/dracula';
|
|
565
565
|
|
|
566
566
|
let { content } = $props();
|
|
567
567
|
</script>
|
|
568
568
|
|
|
569
|
-
<Streamdown
|
|
569
|
+
<Streamdown
|
|
570
|
+
{content}
|
|
571
|
+
components={{ code: Code }}
|
|
572
|
+
highlightThemes={{ dracula }}
|
|
573
|
+
highlightTheme="dracula"
|
|
574
|
+
/>
|
|
570
575
|
```
|
|
571
576
|
|
|
572
|
-
> [!
|
|
573
|
-
>
|
|
577
|
+
> [!NOTE]
|
|
578
|
+
> An unknown `highlightTheme` key falls back to the built-in `github-dark` / `github-light` (following the color scheme) instead of leaving the code block unhighlighted.
|
|
579
|
+
|
|
580
|
+
A custom theme is just an object of the `HighlightTheme` shape (exported from `svelte-streamdown`) — a `name`, `type`, `background`, `foreground` and a color per token class:
|
|
581
|
+
|
|
582
|
+
```svelte
|
|
583
|
+
<script lang="ts">
|
|
584
|
+
import { Streamdown, type HighlightTheme } from 'svelte-streamdown';
|
|
585
|
+
import Code from 'svelte-streamdown/code';
|
|
586
|
+
import githubDark from '@tanstack/highlight/themes/github-dark';
|
|
587
|
+
|
|
588
|
+
const myTheme: HighlightTheme = {
|
|
589
|
+
...githubDark,
|
|
590
|
+
name: 'my-theme',
|
|
591
|
+
tokens: { ...githubDark.tokens, keyword: '#ff0088', comment: '#5a5a5a' }
|
|
592
|
+
};
|
|
593
|
+
</script>
|
|
594
|
+
|
|
595
|
+
<Streamdown
|
|
596
|
+
{content}
|
|
597
|
+
components={{ code: Code }}
|
|
598
|
+
highlightThemes={{ 'my-theme': myTheme }}
|
|
599
|
+
highlightTheme="my-theme"
|
|
600
|
+
/>
|
|
601
|
+
```
|
|
574
602
|
|
|
575
603
|
#### Dynamic (light/dark) theme switching
|
|
576
604
|
|
|
577
|
-
Register every theme you intend to switch between in `
|
|
605
|
+
Register every theme you intend to switch between in `highlightThemes`, then drive `highlightTheme` from your color-scheme store. Switching is fully reactive:
|
|
578
606
|
|
|
579
607
|
```svelte
|
|
580
608
|
<script lang="ts">
|
|
581
609
|
import { Streamdown } from 'svelte-streamdown';
|
|
582
610
|
import Code from 'svelte-streamdown/code';
|
|
583
611
|
import { mode } from 'mode-watcher';
|
|
584
|
-
import
|
|
585
|
-
import
|
|
612
|
+
import oneDarkPro from '@tanstack/highlight/themes/one-dark-pro';
|
|
613
|
+
import solarizedLight from '@tanstack/highlight/themes/solarized-light';
|
|
586
614
|
|
|
587
615
|
let { content } = $props();
|
|
588
616
|
|
|
589
|
-
const
|
|
590
|
-
mode.current === 'dark' ? 'github-dark-default' : 'github-light-default'
|
|
591
|
-
);
|
|
617
|
+
const highlightTheme = $derived(mode.current === 'dark' ? 'one-dark-pro' : 'solarized-light');
|
|
592
618
|
</script>
|
|
593
619
|
|
|
594
620
|
<Streamdown
|
|
595
621
|
{content}
|
|
596
622
|
components={{ code: Code }}
|
|
597
623
|
baseTheme="shadcn"
|
|
598
|
-
|
|
599
|
-
'
|
|
600
|
-
'
|
|
624
|
+
highlightThemes={{
|
|
625
|
+
'one-dark-pro': oneDarkPro,
|
|
626
|
+
'solarized-light': solarizedLight
|
|
601
627
|
}}
|
|
602
|
-
{
|
|
628
|
+
{highlightTheme}
|
|
603
629
|
/>
|
|
604
630
|
```
|
|
605
631
|
|
|
606
632
|
> [!NOTE]
|
|
607
|
-
> The built-in `github-dark` / `github-light` themes can be switched dynamically with just `
|
|
633
|
+
> The built-in `github-dark` / `github-light` themes can be switched dynamically with just `highlightTheme` (no `highlightThemes` registration needed).
|
|
634
|
+
|
|
635
|
+
#### Styling tokens with CSS
|
|
636
|
+
|
|
637
|
+
Every token span carries `th-token` plus a `th-<class>` class (`th-keyword`, `th-string`, `th-comment`, `th-function`, `th-number`, `th-operator`, `th-tag`, `th-attr`, …) alongside its inline color from the active theme, so you can tweak or override token styles from CSS:
|
|
638
|
+
|
|
639
|
+
```css
|
|
640
|
+
[data-streamdown-code] .th-comment {
|
|
641
|
+
font-style: italic;
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Supported languages
|
|
646
|
+
|
|
647
|
+
All 30 scanners ship in the same ~31KB bundle, so there is nothing to lazy-load:
|
|
648
|
+
|
|
649
|
+
`apache`, `cmake`, `cpp`, `css`, `diff`, `dockerfile`, `ejs`, `env`, `go`, `html`, `http`, `js`, `json`, `jsx`, `markdown`, `mermaid`, `nginx`, `php`, `plaintext`, `python`, `scheme`, `shell`, `sql`, `svelte`, `toml`, `ts`, `tsrx`, `tsx`, `vue`, `yaml`.
|
|
650
|
+
|
|
651
|
+
Common aliases are normalized (`javascript` → `js`, `typescript` → `ts`, `bash`/`sh`/`zsh` → `shell`, `py` → `python`, `jsonc`/`json5` → `json`, `xml`/`htm` → `html`, `md` → `markdown`, `yml` → `yaml`, `docker` → `dockerfile`).
|
|
652
|
+
|
|
653
|
+
> [!IMPORTANT]
|
|
654
|
+
> Any other language — including `java`, `rust`, `c`, `c#`, `swift`, `kotlin`, `ruby` and `graphql` — is **not** highlighted as of `@tanstack/highlight@0.1.0`: the code block still renders (and keeps its copy/download UI), but as plaintext.
|
|
655
|
+
|
|
656
|
+
You can add your own language with `defineLanguage` and the `highlightLanguages` prop. A language is a tokenizer returning `{ className, start, end }` ranges:
|
|
657
|
+
|
|
658
|
+
```svelte
|
|
659
|
+
<script lang="ts">
|
|
660
|
+
import { Streamdown, defineLanguage } from 'svelte-streamdown';
|
|
661
|
+
import Code from 'svelte-streamdown/code';
|
|
662
|
+
|
|
663
|
+
const brainfuck = defineLanguage({
|
|
664
|
+
name: 'brainfuck',
|
|
665
|
+
tokenize: (code) =>
|
|
666
|
+
[...code.matchAll(/[+\-<>]+/g)].map((match) => ({
|
|
667
|
+
className: 'operator' as const,
|
|
668
|
+
start: match.index,
|
|
669
|
+
end: match.index + match[0].length
|
|
670
|
+
}))
|
|
671
|
+
});
|
|
672
|
+
</script>
|
|
673
|
+
|
|
674
|
+
<Streamdown {content} components={{ code: Code }} highlightLanguages={[brainfuck]} />
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
> [!NOTE]
|
|
678
|
+
> Keep the array passed to `highlightLanguages` referentially stable (define it outside the template): a new array builds a new highlighter.
|
|
608
679
|
|
|
609
|
-
|
|
680
|
+
### Migrating from v3 (shiki)
|
|
610
681
|
|
|
611
|
-
|
|
682
|
+
v4 replaces shiki with `@tanstack/highlight`. Highlighting is now synchronous, renders during SSR and no longer has a skeleton/loading state. Remove `shiki` / `@shikijs/*` from your dependencies and rename the props:
|
|
612
683
|
|
|
613
684
|
```diff
|
|
614
|
-
-
|
|
615
|
-
|
|
685
|
+
- shikiTheme="nord"
|
|
686
|
+
- shikiThemes={{ nord }}
|
|
687
|
+
- shikiLanguages={[{ id: 'haskell', name: 'Haskell', import: () => import('@shikijs/langs/haskell') }]}
|
|
688
|
+
+ highlightTheme="nord"
|
|
689
|
+
+ highlightThemes={{ nord }}
|
|
690
|
+
+ highlightLanguages={[myLanguage]}
|
|
616
691
|
```
|
|
617
692
|
|
|
618
|
-
|
|
693
|
+
- Theme objects come from `@tanstack/highlight/themes/<name>` instead of `@shikijs/themes/<name>`, and follow the `HighlightTheme` shape (not shiki's `ThemeRegistration`).
|
|
694
|
+
- `highlightLanguages` takes `LanguageDefinition`s built with `defineLanguage`, not lazy shiki grammar loaders.
|
|
695
|
+
- `bundledLanguagesInfo`, `createLanguageSet` and `LanguageInfo` are gone; `defineLanguage`, `LanguageDefinition` and `HighlightTheme` are exported instead.
|
|
696
|
+
- The `code.skeleton` theme key is gone (there is no loading state anymore).
|
|
697
|
+
- Languages shiki covered but `@tanstack/highlight@0.1.0` does not (`java`, `rust`, `c`, `c#`, `swift`, `kotlin`, `ruby`, `graphql`, …) now render as plaintext.
|
|
619
698
|
|
|
620
699
|
## 📋 Props API
|
|
621
700
|
|
|
@@ -628,20 +707,24 @@ This also keeps the default bundle small: only the themes you actually import ar
|
|
|
628
707
|
| `defaultOrigin` | `string` | - | Default origin for relative URLs |
|
|
629
708
|
| `allowedLinkPrefixes` | `string[]` | `['*']` | Allowed URL prefixes for links |
|
|
630
709
|
| `allowedImagePrefixes` | `string[]` | `['*']` | Allowed URL prefixes for images |
|
|
631
|
-
| `
|
|
632
|
-
| `
|
|
633
|
-
| `
|
|
710
|
+
| `renderHtml` | `boolean \| ((token) => string)` | `false` | Render raw HTML blocks and inline tags. When off, the HTML source is shown as literal text instead of being dropped. Pass a function to sanitize and return the HTML string yourself. |
|
|
711
|
+
| `inlineCitationsMode` | `'list' \| 'carousel'` | `'carousel'` | How an inline citation popover presents its sources |
|
|
712
|
+
| `translations` | `{ alert?: { note?, tip?, warning?, caution?, important? } }` | - | Override the built-in alert titles |
|
|
713
|
+
| `icons` | `Partial<Record<IconName, Snippet>>` | - | Replace any built-in icon (`copy`, `check`, `download`, `fullscreen`, `zoomIn`, `zoomOut`, `fitView`, `chevronLeft`, `chevronRight`, `note`, `tip`, `warning`, `caution`, `important`) with your own snippet |
|
|
714
|
+
| `static` | `boolean` | `false` | Render finished content: skips the incomplete-markdown pass and the streaming animation |
|
|
715
|
+
| `element` | `HTMLElement` | - | `bind:element` to get the wrapper node |
|
|
716
|
+
| `streamdown` | `StreamdownContext` | - | `bind:streamdown` to read the resolved context (theme, controls, footnotes, sources) |
|
|
634
717
|
| `theme` | `DeepPartial<Theme>` | - | Custom theme overrides |
|
|
635
718
|
| `baseTheme` | `'tailwind' \| 'shadcn'` | `'tailwind'` | Base theme to use before applying overrides |
|
|
636
719
|
| `mergeTheme` | `boolean` | `true` | Whether to merge theme with base theme |
|
|
637
|
-
| `
|
|
638
|
-
| `
|
|
639
|
-
| `
|
|
720
|
+
| `highlightTheme` | `string` | auto (dark-mode aware) | Code highlighting theme. Defaults to `github-dark` in dark mode / `github-light` otherwise. Any other value must be a key registered via `highlightThemes`. See [Highlight themes](#highlight-themes). |
|
|
721
|
+
| `highlightThemes` | `Record<string, HighlightTheme>` | - | Register additional pre-imported themes (e.g. `{ dracula }`) so they can be selected via `highlightTheme`, including dynamic light/dark switching. |
|
|
722
|
+
| `highlightLanguages` | `LanguageDefinition[]` | - | Additional languages built with `defineLanguage` (merged with the 30 built-in ones) |
|
|
640
723
|
| `mermaidConfig` | `MermaidConfig` | - | Mermaid diagram configuration |
|
|
641
724
|
| `katexConfig` | `KatexOptions \| ((inline: boolean) => KatexOptions)` | - | KaTeX math rendering options |
|
|
642
725
|
| `animation` | `AnimationConfig` | - | Animation configuration for streaming content |
|
|
643
726
|
| `animation.enabled` | `boolean` | `false` | Enable/disable animations |
|
|
644
|
-
| `animation.type` | `'fade' \| 'blur' \| '
|
|
727
|
+
| `animation.type` | `'fade' \| 'blur' \| 'slideUp' \| 'slideDown'` | `'blur'` | Animation style for text appearance |
|
|
645
728
|
| `animation.duration` | `number` | `500` | Animation duration in milliseconds |
|
|
646
729
|
| `animation.timingFunction` | `'ease' \| 'ease-in' \| 'ease-out' \| 'ease-in-out' \| 'linear'` | `'ease-in'` | CSS timing function for animations |
|
|
647
730
|
| `animation.tokenize` | `'word' \| 'char'` | `'word'` | Tokenization method for text animations |
|
|
@@ -660,7 +743,7 @@ This also keeps the default bundle small: only the themes you actually import ar
|
|
|
660
743
|
|
|
661
744
|
**Lists**: `ul`, `ol`, `li`
|
|
662
745
|
|
|
663
|
-
**Code**: `code`, `
|
|
746
|
+
**Code**: `code`, `codespan`
|
|
664
747
|
|
|
665
748
|
**Tables**: `table`, `thead`, `tbody`, `tr`, `th`, `td`, `tfoot`
|
|
666
749
|
|
|
@@ -753,7 +836,7 @@ Each component supports multiple themeable parts:
|
|
|
753
836
|
|
|
754
837
|
**Links (`a`)**: `base`, `blocked` (for blocked/unsafe links)
|
|
755
838
|
|
|
756
|
-
**Code (`code`)**: `base`, `container`, `header`, `
|
|
839
|
+
**Code (`code`)**: `base`, `container`, `header`, `buttons`, `language`, `line`, `pre`
|
|
757
840
|
|
|
758
841
|
**Inline Code (`inlineCode`)**: `base`
|
|
759
842
|
|
|
@@ -1024,6 +1107,10 @@ pnpm dev
|
|
|
1024
1107
|
# Run tests
|
|
1025
1108
|
pnpm test
|
|
1026
1109
|
|
|
1110
|
+
# Run the browser (component) tests — needs a Chromium binary:
|
|
1111
|
+
# pnpm exec playwright install chromium
|
|
1112
|
+
pnpm test:browser
|
|
1113
|
+
|
|
1027
1114
|
# Build for production
|
|
1028
1115
|
pnpm build
|
|
1029
1116
|
```
|
package/dist/Block.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { parseIncompleteMarkdown } from './utils/parse-incomplete-markdown.js';
|
|
2
|
+
import { parseIncompleteMarkdown as completeMarkdown } from './utils/parse-incomplete-markdown.js';
|
|
3
3
|
import Element from './Elements/Element.svelte';
|
|
4
4
|
import { lex, type StreamdownToken } from './marked/index.js';
|
|
5
5
|
import AnimatedText from './AnimatedText.svelte';
|
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
} = $props();
|
|
16
16
|
|
|
17
17
|
const streamdown = useStreamdown();
|
|
18
|
+
// The old code never consulted `streamdown.parseIncompleteMarkdown`; the import
|
|
19
|
+
// is aliased so the context flag and the helper cannot be confused.
|
|
20
|
+
const complete = $derived(!isStatic && streamdown.parseIncompleteMarkdown !== false);
|
|
18
21
|
const tokens = $derived(
|
|
19
|
-
lex(
|
|
22
|
+
lex(complete ? completeMarkdown(block.trim()) : block, streamdown.extensions)
|
|
20
23
|
);
|
|
21
24
|
const insidePopover = getContext('POPOVER');
|
|
22
25
|
</script>
|
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
style:position="relative"
|
|
175
175
|
style:transition-duration="200ms"
|
|
176
176
|
style:transition-timing-function="ease-in-out"
|
|
177
|
-
aria-label=
|
|
177
|
+
aria-label={'Citations-' + id}
|
|
178
178
|
>
|
|
179
179
|
<div
|
|
180
180
|
bind:this={stepper.stepContainer}
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
style:height="fit-content"
|
|
194
194
|
style:width="100%"
|
|
195
195
|
style:flex-grow="1"
|
|
196
|
-
aria-label=
|
|
196
|
+
aria-label={'Citation-' + id}
|
|
197
197
|
>
|
|
198
198
|
<Slot render={streamdown.snippets.inlineCitationContent} props={{ source, key, token }}>
|
|
199
199
|
{#if url || title}
|
|
@@ -2,31 +2,28 @@
|
|
|
2
2
|
import { useStreamdown } from '../context.svelte.js';
|
|
3
3
|
import { save } from '../utils/save.js';
|
|
4
4
|
import { useCopy } from '../utils/copy.svelte.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import type { Tokens } from 'marked';
|
|
8
|
-
import { type ThemedToken } from 'shiki';
|
|
9
|
-
import { untrack } from 'svelte';
|
|
5
|
+
import { highlightLines, languageExtensionMap } from '../utils/hightlighter.svelte.js';
|
|
6
|
+
import type { CodeToken } from '../marked/index.js';
|
|
10
7
|
import { checkIcon, copyIcon, downloadIcon } from './icons.js';
|
|
11
8
|
|
|
12
9
|
const {
|
|
13
10
|
token,
|
|
14
11
|
id
|
|
15
12
|
}: {
|
|
16
|
-
token:
|
|
13
|
+
token: CodeToken;
|
|
17
14
|
id: string;
|
|
18
15
|
} = $props();
|
|
19
16
|
|
|
20
17
|
const streamdown = useStreamdown();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
);
|
|
18
|
+
|
|
19
|
+
// marked keeps the fence's trailing blank lines in `text`; they render as empty
|
|
20
|
+
// lines and, while streaming, flicker in and out on nearly every chunk. Render,
|
|
21
|
+
// copy and download all read this so they can never disagree.
|
|
22
|
+
const code = $derived(token.text.replace(/\n+$/, ''));
|
|
26
23
|
|
|
27
24
|
const copy = useCopy({
|
|
28
25
|
get content() {
|
|
29
|
-
return
|
|
26
|
+
return code;
|
|
30
27
|
}
|
|
31
28
|
});
|
|
32
29
|
|
|
@@ -40,19 +37,13 @@
|
|
|
40
37
|
: 'txt';
|
|
41
38
|
const filename = `file.${extension}`;
|
|
42
39
|
const mimeType = 'text/plain';
|
|
43
|
-
save(filename,
|
|
40
|
+
save(filename, code, mimeType);
|
|
44
41
|
} catch (error) {
|
|
45
42
|
console.error('Failed to download file:', error);
|
|
46
43
|
}
|
|
47
44
|
};
|
|
48
45
|
|
|
49
|
-
$
|
|
50
|
-
const theme = streamdown.shikiTheme;
|
|
51
|
-
const lang = token.lang;
|
|
52
|
-
untrack(() => {
|
|
53
|
-
void highlighter.load(theme, lang);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
46
|
+
const lines = $derived(highlightLines(code, token.lang, streamdown.highlightLanguages));
|
|
56
47
|
</script>
|
|
57
48
|
|
|
58
49
|
<div
|
|
@@ -84,39 +75,15 @@
|
|
|
84
75
|
{/if}
|
|
85
76
|
</div>
|
|
86
77
|
<div style="height: fit-content; width: 100%;" class={streamdown.theme.code.container}>
|
|
87
|
-
{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
78
|
+
<pre class={streamdown.theme.code.pre}><code
|
|
79
|
+
>{#each lines as line}<span class={streamdown.theme.code.line}
|
|
80
|
+
>{#if line.length === 0}​{/if}{#each line as t}<span
|
|
81
|
+
class="th-token{t.className ? ` th-${t.className}` : ''}"
|
|
82
|
+
style={streamdown.isMounted ? streamdown.animationTextStyle : ''}
|
|
83
|
+
style:color={streamdown.highlightTheme.tokens[t.className ?? 'token']}
|
|
84
|
+
>{t.value}</span
|
|
85
|
+
>{/each}</span
|
|
86
|
+
>{/each}</code
|
|
87
|
+
></pre>
|
|
97
88
|
</div>
|
|
98
89
|
</div>
|
|
99
|
-
|
|
100
|
-
{#snippet Tokens(lines: ThemedToken[][])}
|
|
101
|
-
{#each lines as tokens}
|
|
102
|
-
<span class={streamdown.theme.code.line}>
|
|
103
|
-
{#each tokens as token}
|
|
104
|
-
<span
|
|
105
|
-
style={streamdown.isMounted ? streamdown.animationTextStyle : ''}
|
|
106
|
-
style:color={token.color}
|
|
107
|
-
style:background-color={token.bgColor}
|
|
108
|
-
>
|
|
109
|
-
{token.content}
|
|
110
|
-
</span>
|
|
111
|
-
{/each}
|
|
112
|
-
</span>
|
|
113
|
-
{/each}
|
|
114
|
-
{/snippet}
|
|
115
|
-
|
|
116
|
-
{#snippet Skeleton(lines: string[])}
|
|
117
|
-
{#each lines as line}
|
|
118
|
-
<span class={streamdown.theme.code.skeleton}>
|
|
119
|
-
{line.trim().length > 0 ? line : '\u200B'}
|
|
120
|
-
</span>
|
|
121
|
-
{/each}
|
|
122
|
-
{/snippet}
|
|
@@ -288,12 +288,11 @@
|
|
|
288
288
|
{@render children()}
|
|
289
289
|
</dd>
|
|
290
290
|
</Slot>
|
|
291
|
-
{:else if token.type === 'def'}
|
|
292
|
-
<!--
|
|
291
|
+
{:else if token.type === 'def' || token.type === 'space'}
|
|
292
|
+
<!-- Link reference definitions and blank lines produce no output (CommonMark) -->
|
|
293
293
|
{:else if token.type === 'escape'}
|
|
294
|
-
<!--
|
|
295
|
-
{
|
|
296
|
-
<!-- TODO This does not seems to be tokenized for now -->
|
|
294
|
+
<!-- `children` renders token.text, i.e. the escaped character itself -->
|
|
295
|
+
{@render children()}
|
|
297
296
|
{:else if token.type === 'text'}
|
|
298
297
|
{@render children()}
|
|
299
298
|
{:else if token.type === 'html'}
|
|
@@ -301,6 +300,10 @@
|
|
|
301
300
|
{@const content =
|
|
302
301
|
typeof streamdown.renderHtml === 'function' ? streamdown.renderHtml(token) : token.raw}
|
|
303
302
|
{@html content}
|
|
303
|
+
{:else}
|
|
304
|
+
<!-- Without renderHtml the source is shown literally instead of being dropped;
|
|
305
|
+
`children` interpolates it as text, so Svelte escapes it — no XSS surface -->
|
|
306
|
+
{@render children()}
|
|
304
307
|
{/if}
|
|
305
308
|
{:else if token.type === 'mdx'}
|
|
306
309
|
{@const Component = streamdown.mdxComponents?.[token.tagName]}
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
<a
|
|
40
40
|
data-streamdown-link={id}
|
|
41
41
|
class={streamdown.theme.link.base}
|
|
42
|
+
title={token.title}
|
|
42
43
|
{...isRelativeUrl
|
|
43
44
|
? { href: token.href }
|
|
44
45
|
: { href: transformedUrl, target: '_blank', rel: 'noopener noreferrer' }}
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
<span
|
|
51
52
|
data-streamdown-link-blocked={id}
|
|
52
53
|
class={streamdown.theme.link.blocked}
|
|
53
|
-
title={
|
|
54
|
+
title={`Blocked URL: ${token.href}`}
|
|
54
55
|
>
|
|
55
56
|
{@render children()} [blocked]
|
|
56
57
|
</span>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from 'svelte';
|
|
3
3
|
import { useStreamdown } from '../context.svelte.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { CodeToken } from '../marked/index.js';
|
|
5
5
|
import type { MermaidConfig } from 'mermaid';
|
|
6
6
|
import { on } from 'svelte/events';
|
|
7
7
|
import { usePanzoom } from '../utils/panzoom.svelte';
|
|
@@ -14,10 +14,15 @@
|
|
|
14
14
|
token,
|
|
15
15
|
id
|
|
16
16
|
}: {
|
|
17
|
-
token:
|
|
17
|
+
token: CodeToken;
|
|
18
18
|
id: string;
|
|
19
19
|
} = $props();
|
|
20
20
|
|
|
21
|
+
// Trailing blank lines are noise for mermaid but they still changed `token.text`
|
|
22
|
+
// on nearly every streamed chunk, which re-ran the whole render. Same trim as
|
|
23
|
+
// Code.svelte.
|
|
24
|
+
const chart = $derived(token.text.replace(/\n+$/, ''));
|
|
25
|
+
|
|
21
26
|
let mermaid = $state<any>(null);
|
|
22
27
|
onMount(async () => {
|
|
23
28
|
mermaid = (await import('mermaid')).default;
|
|
@@ -217,7 +222,7 @@
|
|
|
217
222
|
<div
|
|
218
223
|
style={streamdown.isMounted ? streamdown.animationBlockStyle : ''}
|
|
219
224
|
class={streamdown.theme.mermaid.base}
|
|
220
|
-
{@attach (node) => renderMermaid(
|
|
225
|
+
{@attach (node) => renderMermaid(chart, node)}
|
|
221
226
|
{@attach insider.attach}
|
|
222
227
|
data-expanded={'false'}
|
|
223
228
|
>
|
|
@@ -46,6 +46,15 @@
|
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
// textContent flattens a real <br> element, so a multiline cell collapses to
|
|
50
|
+
// 'Paragraph one.Paragraph two.'. Walk the cell instead and keep the breaks;
|
|
51
|
+
// the quoting below then quotes the newline for us.
|
|
52
|
+
const extractCellText = (node: Node): string => {
|
|
53
|
+
if (node.nodeType === 3) return node.textContent || '';
|
|
54
|
+
if ((node as Element).tagName === 'BR') return '\n';
|
|
55
|
+
return Array.from(node.childNodes).map(extractCellText).join('');
|
|
56
|
+
};
|
|
57
|
+
|
|
49
58
|
const copyOrDownload = (type: 'Markdown' | 'HTML' | 'CSV') => {
|
|
50
59
|
if (type === 'Markdown') {
|
|
51
60
|
copyValue = token.raw;
|
|
@@ -55,7 +64,7 @@
|
|
|
55
64
|
save('table.md', copyValue, 'text/markdown');
|
|
56
65
|
}
|
|
57
66
|
} else if (type === 'HTML') {
|
|
58
|
-
const table = document.querySelector(`[data-streamdown-table
|
|
67
|
+
const table = document.querySelector(`[data-streamdown-table="${id}"]`);
|
|
59
68
|
|
|
60
69
|
if (table) {
|
|
61
70
|
let html = (table.cloneNode(true) as HTMLElement).outerHTML;
|
|
@@ -83,7 +92,7 @@
|
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
} else if (type === 'CSV') {
|
|
86
|
-
const table = document.querySelector(`[data-streamdown-table
|
|
95
|
+
const table = document.querySelector(`[data-streamdown-table="${id}"]`);
|
|
87
96
|
|
|
88
97
|
if (table) {
|
|
89
98
|
const rows = table.querySelectorAll('tr');
|
|
@@ -98,9 +107,8 @@
|
|
|
98
107
|
const colSpan = parseInt(cell.getAttribute('colspan') || '1');
|
|
99
108
|
const rowSpan = parseInt(cell.getAttribute('rowspan') || '1');
|
|
100
109
|
|
|
101
|
-
// Add the cell content
|
|
102
110
|
// Add the cell content, quoting if it contains commas, quotes, or newlines
|
|
103
|
-
const content = cell
|
|
111
|
+
const content = extractCellText(cell);
|
|
104
112
|
const needsQuoting = /[,"\n]/.test(content);
|
|
105
113
|
const escapedContent = content.replace(/"/g, '""');
|
|
106
114
|
rowData.push(needsQuoting ? `"${escapedContent}"` : content);
|
|
@@ -150,7 +158,7 @@
|
|
|
150
158
|
|
|
151
159
|
{#if popover.isOpen}
|
|
152
160
|
<dialog
|
|
153
|
-
id={'table-download-popover'}
|
|
161
|
+
id={'table-download-popover-' + id}
|
|
154
162
|
aria-modal="false"
|
|
155
163
|
transition:scale|global={{ start: 0.95, duration: 100 }}
|
|
156
164
|
{@attach clickOutside.attachment}
|