svelte-streamdown 1.0.9 → 2.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.
Files changed (37) hide show
  1. package/README.md +160 -143
  2. package/dist/Block.svelte +12 -72
  3. package/dist/Elements/Alert.svelte +1 -2
  4. package/dist/Elements/Code.svelte +7 -7
  5. package/dist/Elements/Code.svelte.d.ts +0 -2
  6. package/dist/Elements/Element.svelte +70 -25
  7. package/dist/Elements/FootnoteRef.svelte +19 -6
  8. package/dist/Elements/Image.svelte +1 -1
  9. package/dist/Elements/Link.svelte +1 -1
  10. package/dist/Elements/Math.svelte +13 -25
  11. package/dist/Elements/Math.svelte.d.ts +0 -2
  12. package/dist/Elements/Mermaid.svelte +105 -115
  13. package/dist/Elements/Mermaid.svelte.d.ts +0 -2
  14. package/dist/Elements/Table.svelte +1 -1
  15. package/dist/Streamdown.d.ts +52 -10
  16. package/dist/Streamdown.js +19 -1
  17. package/dist/Streamdown.svelte +3 -41
  18. package/dist/Streamdown.svelte.d.ts +1 -20
  19. package/dist/marked/index.d.ts +3 -20
  20. package/dist/marked/index.js +24 -19
  21. package/dist/marked/marked-alert.d.ts +2 -2
  22. package/dist/marked/marked-alert.js +7 -21
  23. package/dist/marked/marked-footnotes.js +2 -3
  24. package/dist/marked/marked-list.d.ts +2 -2
  25. package/dist/marked/marked-list.js +28 -11
  26. package/dist/marked/marked-math.js +1 -1
  27. package/dist/marked/marked-subsup.d.ts +1 -1
  28. package/dist/marked/marked-subsup.js +2 -1
  29. package/dist/marked/marked-table.d.ts +66 -0
  30. package/dist/marked/marked-table.js +495 -0
  31. package/dist/theme.d.ts +24 -6
  32. package/dist/theme.js +26 -14
  33. package/dist/utils/panzoom.svelte.d.ts +17 -18
  34. package/dist/utils/panzoom.svelte.js +0 -2
  35. package/dist/utils/useClickOutside.svelte.js +0 -1
  36. package/dist/utils/useKeyDown.svelte.js +4 -1
  37. package/package.json +1 -2
@@ -1 +1,19 @@
1
- export {};
1
+ import { getContext, setContext } from 'svelte';
2
+ export class StreamdownContext {
3
+ footnotes = {
4
+ refs: new Map(),
5
+ footnotes: new Map()
6
+ };
7
+ constructor(props) {
8
+ bind(this, props);
9
+ setContext('streamdown', this);
10
+ }
11
+ }
12
+ export const useStreamdown = () => {
13
+ const context = getContext('streamdown');
14
+ if (!context) {
15
+ throw new Error('Streamdown context not found');
16
+ }
17
+ return context;
18
+ };
19
+ import { bind } from './utils/bind.js';
@@ -1,42 +1,10 @@
1
- <script lang="ts" module>
2
- import { getContext, setContext } from 'svelte';
3
-
4
- export interface StreamdownContext
5
- extends Omit<StreamdownProps, keyof Snippets | 'class' | 'theme' | 'shikiTheme'> {
6
- snippets: Snippets;
7
- shikiTheme: BundledTheme;
8
- theme: Theme;
9
- }
10
- export class StreamdownContext {
11
- footnotes = {
12
- refs: new Map<string, FootnoteRef>(),
13
- footnotes: new Map<string, Footnote>()
14
- };
15
- constructor(props: Omit<StreamdownProps, keyof Snippets | 'class'> & { snippets: Snippets }) {
16
- bind(this, props);
17
- setContext('streamdown', this);
18
- }
19
- }
20
- export const useStreamdown = () => {
21
- const context = getContext<StreamdownContext>('streamdown');
22
- if (!context) {
23
- throw new Error('Streamdown context not found');
24
- }
25
- return context;
26
- };
27
- </script>
28
-
29
1
  <script lang="ts">
30
2
  import Block from './Block.svelte';
31
- import type { Snippets, StreamdownProps } from './Streamdown.js';
32
- import { bind } from './utils/bind.js';
3
+ import { StreamdownContext, type StreamdownProps } from './Streamdown.js';
33
4
  import 'katex/dist/katex.min.css';
34
- import { mergeTheme, type Theme, shadcnTheme } from './theme.js';
35
- import type { BundledTheme } from 'shiki';
5
+ import { mergeTheme, shadcnTheme } from './theme.js';
36
6
  import { parseBlocks } from './marked/index.js';
37
- import { SvelteMap } from 'svelte/reactivity';
38
- import type { FootnoteRef } from './marked/marked-footnotes.js';
39
- import type { Footnote } from './marked/marked-footnotes.js';
7
+
40
8
  let {
41
9
  content = '',
42
10
  class: className,
@@ -52,13 +20,10 @@
52
20
  translations,
53
21
  baseTheme,
54
22
  mergeTheme: shouldMergeTheme = true,
55
- customElements,
56
23
  streamdown = $bindable(),
57
24
  ...snippets
58
25
  }: StreamdownProps = $props();
59
26
 
60
- console.log('render');
61
-
62
27
  streamdown = new StreamdownContext({
63
28
  get content() {
64
29
  return content;
@@ -98,9 +63,6 @@
98
63
  get translations() {
99
64
  return translations;
100
65
  },
101
- get customElements() {
102
- return customElements;
103
- },
104
66
  get shikiPreloadThemes() {
105
67
  return shikiPreloadThemes;
106
68
  }
@@ -1,24 +1,5 @@
1
- export interface StreamdownContext extends Omit<StreamdownProps, keyof Snippets | 'class' | 'theme' | 'shikiTheme'> {
2
- snippets: Snippets;
3
- shikiTheme: BundledTheme;
4
- theme: Theme;
5
- }
6
- export declare class StreamdownContext {
7
- footnotes: {
8
- refs: Map<string, FootnoteRef>;
9
- footnotes: Map<string, Footnote>;
10
- };
11
- constructor(props: Omit<StreamdownProps, keyof Snippets | 'class'> & {
12
- snippets: Snippets;
13
- });
14
- }
15
- export declare const useStreamdown: () => StreamdownContext;
16
- import type { Snippets, StreamdownProps } from './Streamdown.js';
1
+ import { type StreamdownProps } from './Streamdown.js';
17
2
  import 'katex/dist/katex.min.css';
18
- import { type Theme } from './theme.js';
19
- import type { BundledTheme } from 'shiki';
20
- import type { FootnoteRef } from './marked/marked-footnotes.js';
21
- import type { Footnote } from './marked/marked-footnotes.js';
22
3
  declare const Streamdown: import("svelte").Component<StreamdownProps, {}, "streamdown">;
23
4
  type Streamdown = ReturnType<typeof Streamdown>;
24
5
  export default Streamdown;
@@ -4,26 +4,9 @@ import { type FootnoteToken } from './marked-footnotes.js';
4
4
  import { type MathToken } from './marked-math.js';
5
5
  import { type SubSupToken } from './marked-subsup.js';
6
6
  import { type ListItemToken, type ListToken } from './marked-list.js';
7
- export type StreamdownToken = Exclude<MarkedToken, Tokens.List | Tokens.ListItem> | ListToken | ListItemToken | MathToken | AlertToken | FootnoteToken | SubSupToken | TableRow | TD | TH | TableHead;
8
- export type TD = Tokens.TableCell & {
9
- type: 'td';
10
- raw: string;
11
- };
12
- export type TH = Tokens.TableCell & {
13
- type: 'th';
14
- raw: string;
15
- };
16
- export type TableRow = {
17
- type: 'tableRow';
18
- raw: string;
19
- tokens: (TD | TH)[];
20
- isHeader: boolean;
21
- };
22
- export type TableHead = {
23
- type: 'tableHead';
24
- raw: string;
25
- tokens: TableRow[];
26
- };
7
+ import { type TableToken, type THead, type TBody, type TFoot, type THeadRow, type TRow, type TH, type TD } from './marked-table.js';
8
+ export type StreamdownToken = Exclude<MarkedToken, Tokens.List | Tokens.ListItem> | ListToken | ListItemToken | MathToken | AlertToken | FootnoteToken | SubSupToken | TableToken | THead | TBody | TFoot | THeadRow | TRow | TH | TD;
9
+ export type { TableToken, THead, TBody, TFoot, THeadRow, TRow, TH, TD } from './marked-table.js';
27
10
  export declare const lex: (markdown: string) => StreamdownToken[];
28
11
  export declare const parseBlocks: (markdown: string) => string[];
29
12
  export type { MathToken, AlertToken, FootnoteToken, SubSupToken };
@@ -1,32 +1,37 @@
1
- import { Lexer, marked } from 'marked';
1
+ import { Lexer, Marked } from 'marked';
2
2
  import markedAlert, {} from './marked-alert.js';
3
3
  import markedFootnote, {} from './marked-footnotes.js';
4
4
  import { markedMath } from './marked-math.js';
5
5
  import markedSubSup, {} from './marked-subsup.js';
6
6
  import { markedList } from './marked-list.js';
7
+ import { markedTable } from './marked-table.js';
8
+ const completeLexer = new Marked({
9
+ tokenizer: {
10
+ table: () => {
11
+ return false;
12
+ }
13
+ }
14
+ })
15
+ .use(markedAlert())
16
+ .use(markedFootnote())
17
+ .use(markedMath())
18
+ .use(markedSubSup())
19
+ .use(markedList())
20
+ .use(markedTable());
21
+ const blockLexer = new Lexer({
22
+ extensions: {
23
+ childTokens: {},
24
+ renderers: {},
25
+ block: [markedFootnote().extensions[0].tokenizer, markedTable().extensions[0].tokenizer]
26
+ }
27
+ });
7
28
  export const lex = (markdown) => {
8
- return marked
9
- .use({
10
- gfm: true
11
- })
12
- .use(markedAlert())
13
- .use(markedFootnote())
14
- .use(markedMath())
15
- .use(markedSubSup())
16
- .use(markedList())
29
+ return completeLexer
17
30
  .lexer(markdown)
18
31
  .filter((token) => token.type !== 'space' && token.type !== 'footnote');
19
32
  };
20
33
  export const parseBlocks = (markdown) => {
21
- const lexer = new Lexer({
22
- gfm: true,
23
- extensions: {
24
- childTokens: {},
25
- renderers: {},
26
- block: [markedFootnote().extensions[0].tokenizer]
27
- }
28
- });
29
- return lexer.blockTokens(markdown, []).reduce((acc, block) => {
34
+ return blockLexer.blockTokens(markdown, []).reduce((acc, block) => {
30
35
  if (block.type === 'space' || block.type === 'footnote') {
31
36
  return acc;
32
37
  }
@@ -1,4 +1,4 @@
1
- import type { Token, Tokenizer } from 'marked';
1
+ import type { Tokenizer, Tokens } from 'marked';
2
2
  import type { TokenizerExtensionFunction } from 'marked';
3
3
  type variantType = 'note' | 'tip' | 'important' | 'warning' | 'caution';
4
4
  export declare function createSyntaxPattern(type: variantType): string;
@@ -9,7 +9,7 @@ export default function markedAlert(): {
9
9
  tokenizer: TokenizerExtensionFunction;
10
10
  }>;
11
11
  };
12
- export declare function processAlertToken(token: Token, tokenizer: Tokenizer): void;
12
+ export declare function processAlertToken(token: Tokens.Blockquote, tokenizer: Tokenizer): void;
13
13
  export type AlertToken = {
14
14
  type: 'alert';
15
15
  variant: variantType;
@@ -1,7 +1,7 @@
1
1
  import { Lexer } from 'marked';
2
2
  const variants = ['note', 'tip', 'important', 'warning', 'caution'];
3
3
  export function createSyntaxPattern(type) {
4
- return `^(?:\\[!${type.toUpperCase()}])\\s*?\n*`;
4
+ return `^\\s*\\[!${type.toUpperCase()}\\]\\s+`;
5
5
  }
6
6
  export default function markedAlert() {
7
7
  const defaultLexer = new Lexer({ gfm: true });
@@ -11,7 +11,7 @@ export default function markedAlert() {
11
11
  {
12
12
  name: 'alert',
13
13
  level: 'block',
14
- tokenizer(src, tokens) {
14
+ tokenizer(src) {
15
15
  const cap = defaultTokenizer.rules.block.blockquote.exec(src);
16
16
  if (cap) {
17
17
  const blockquoteToken = defaultTokenizer.blockquote(src);
@@ -27,25 +27,11 @@ export function processAlertToken(token, tokenizer) {
27
27
  const matchedVariant = variants.find((type) => new RegExp(createSyntaxPattern(type)).test(('text' in token && token.text) || ''));
28
28
  if (!matchedVariant)
29
29
  return;
30
- const typeRegexp = new RegExp(createSyntaxPattern(matchedVariant));
31
- // Transform the token into an alert token
32
- Object.assign(token, {
33
- type: 'alert',
34
- variant: matchedVariant
35
- });
36
- const firstLine = ('tokens' in token && token.tokens?.[0]);
37
- const firstLineText = firstLine.raw?.replace(typeRegexp, '').trim();
38
- const lines = token.raw
39
- .split('>')
40
- .filter((line) => line.trim().replace(typeRegexp, '').length > 0);
41
- const tokens = lines.map((line) => {
42
- const lineTokens = tokenizer.lexer.inlineTokens(line);
43
- return {
44
- type: 'paragraph',
45
- tokens: lineTokens
46
- };
47
- });
48
- // Transform the token into an alert token
30
+ const tokens = token.tokens
31
+ .map((token) => {
32
+ return tokenizer.lexer.blockTokens(token.raw.replaceAll(`[!${matchedVariant.toUpperCase()}]`, '').trim(), [])[0];
33
+ })
34
+ .filter(Boolean);
49
35
  Object.assign(token, {
50
36
  type: 'alert',
51
37
  variant: matchedVariant,
@@ -1,5 +1,5 @@
1
- import { lex } from './index.js';
2
- import { StreamdownContext } from '../Streamdown.svelte';
1
+ import {} from './index.js';
2
+ import { StreamdownContext } from '../Streamdown.js';
3
3
  import { getContext } from 'svelte';
4
4
  const safeGetContext = () => {
5
5
  try {
@@ -72,7 +72,6 @@ export default function markedFootnote() {
72
72
  if (match) {
73
73
  const [raw, label] = match;
74
74
  const footnote = maps.footnotes.get(label);
75
- console.log({ footnote });
76
75
  const token = {
77
76
  type: 'footnoteRef',
78
77
  raw,
@@ -16,9 +16,9 @@ export interface ListToken {
16
16
  type: 'list';
17
17
  raw: string;
18
18
  ordered: boolean;
19
- listType: 'decimal' | 'lower-alpha' | 'upper-alpha' | 'lower-roman' | 'upper-roman';
19
+ listType: 'decimal' | 'lower-alpha' | 'upper-alpha' | 'lower-roman' | 'upper-roman' | null;
20
20
  loose: boolean;
21
- items: ListItemToken[];
21
+ tokens: ListItemToken[];
22
22
  }
23
23
  /**
24
24
  * Token representing an item in an extended list.
@@ -27,25 +27,28 @@ export const romanLower = '(?:c|xc|l?x{0,3}(?:ix|iv|v?i{0,3}))';
27
27
  export const bulletPattern = `(?:[*+-]|(?:\\d{1,9}|[a-zA-Z]|${romanUpper}|${romanLower})[.)])`;
28
28
  export const rule = `^( {0,3}${bulletPattern})([ \\t][^\\n]+?)?(?:\\n|$)`;
29
29
  function finalizeList(list, lexer) {
30
- if (list.items.length === 0)
30
+ if (list.tokens.length === 0)
31
31
  return;
32
32
  // Trim trailing newline from last item
33
- const lastItem = list.items[list.items.length - 1];
33
+ const lastItem = list.tokens[list.tokens.length - 1];
34
34
  lastItem.raw = lastItem.raw.trimEnd();
35
35
  lastItem.text = lastItem.text.trimEnd();
36
36
  list.raw = list.raw.trimEnd();
37
37
  // Handle child tokens
38
- for (const item of list.items) {
38
+ for (const item of list.tokens) {
39
39
  lexer.state.top = false;
40
40
  item.tokens = lexer.blockTokens(item.text, []);
41
41
  }
42
42
  // Mark list as loose if needed
43
43
  if (list.loose) {
44
- for (const item of list.items) {
44
+ for (const item of list.tokens) {
45
45
  item.loose = true;
46
46
  }
47
47
  }
48
48
  }
49
+ function escapeForRegex(s) {
50
+ return s.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
51
+ }
49
52
  export function markedList() {
50
53
  return {
51
54
  extensions: [
@@ -60,7 +63,7 @@ export function markedList() {
60
63
  const isOrdered = bullet !== '*' && bullet !== '-' && bullet !== '+';
61
64
  let bull;
62
65
  let type = '';
63
- let expectedValue = 1;
66
+ let expectedValue = null;
64
67
  // Detect list type (Roman, alphabetic, numeric)
65
68
  if (isOrdered) {
66
69
  if (bullet.match(new RegExp(`^${romanUpper}[.)]$`))) {
@@ -86,14 +89,15 @@ export function markedList() {
86
89
  }
87
90
  else {
88
91
  bull = this.lexer.options.pedantic ? bullet : '[*+-]';
92
+ bull = this.lexer.options.pedantic ? escapeForRegex(bullet) : '[*+-]';
89
93
  }
90
94
  const list = {
91
95
  type: 'list',
92
96
  raw: '',
93
97
  ordered: isOrdered,
94
- listType: type,
98
+ listType: isOrdered ? type : null,
95
99
  loose: false,
96
- items: []
100
+ tokens: []
97
101
  };
98
102
  // Get next list item
99
103
  const itemRegex = new RegExp(`^( {0,3}${bull})((?:[\t ][^\\n]*)?(?:\\n|$))`);
@@ -189,7 +193,21 @@ export function markedList() {
189
193
  else if (type === 'lower-roman' || type === 'upper-roman') {
190
194
  value = romanToInt(bullet.slice(0, -1));
191
195
  }
192
- list.items.push({
196
+ // Handle expectedValue initialization and validation
197
+ let skipped = false;
198
+ if (isOrdered) {
199
+ if (expectedValue === null) {
200
+ // First item: set expectedValue to this item's value (or 1 if parsing failed)
201
+ expectedValue = value ?? 1;
202
+ }
203
+ else {
204
+ // Subsequent items: check if value matches expected
205
+ skipped = value !== null && value !== expectedValue;
206
+ // Increment expectedValue for next item
207
+ expectedValue += 1;
208
+ }
209
+ }
210
+ list.tokens.push({
193
211
  type: 'list_item',
194
212
  raw,
195
213
  task: !!isTask,
@@ -197,13 +215,12 @@ export function markedList() {
197
215
  loose: false,
198
216
  text: itemContents,
199
217
  value,
200
- skipped: isOrdered && value !== expectedValue,
218
+ skipped,
201
219
  tokens: []
202
220
  });
203
- expectedValue = (value ?? 0) + 1;
204
221
  list.raw += raw;
205
222
  }
206
- if (list.items.length === 0)
223
+ if (list.tokens.length === 0)
207
224
  return null;
208
225
  // Finalize the list
209
226
  finalizeList(list, this.lexer);
@@ -1,6 +1,6 @@
1
1
  const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:?!。,:]|$)/;
2
2
  const inlineRuleNonStandard = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1/; // Non-standard, even if there are no spaces before and after $ or $$, try to parse
3
- const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/;
3
+ const blockRule = /^(\${1,2})\n((?:\\[\s\S]|[^\\])+?)\n\1(?:\n|$)/;
4
4
  export function markedMath() {
5
5
  return {
6
6
  extensions: [
@@ -4,7 +4,7 @@ export default function markedSubSup(): {
4
4
  name: string;
5
5
  level: 'inline';
6
6
  tokenizer: TokenizerExtensionFunction;
7
- start?: (src: string) => number | void;
7
+ start?: (src: string) => number | undefined;
8
8
  }[];
9
9
  };
10
10
  /**
@@ -7,7 +7,8 @@ export default function markedSubSup() {
7
7
  name: 'sub',
8
8
  level: 'inline',
9
9
  start(src) {
10
- return src.indexOf('~');
10
+ const i = src.indexOf('~');
11
+ return i === -1 ? undefined : i;
11
12
  },
12
13
  tokenizer(src) {
13
14
  const match = src.match(subRule);
@@ -0,0 +1,66 @@
1
+ import type { TokenizerExtensionFunction } from 'marked';
2
+ export interface SpanTableOptions {
3
+ useTheadTbody?: boolean;
4
+ useTfoot?: boolean;
5
+ detectFooter?: boolean;
6
+ maxColspan?: number | null;
7
+ handleComplexSpans?: boolean;
8
+ }
9
+ interface BaseCell {
10
+ rowspan: number;
11
+ colspan: number;
12
+ text: string;
13
+ position?: number;
14
+ tokens?: unknown[];
15
+ rowSpanTarget?: BaseCell;
16
+ complexRowSpan?: boolean;
17
+ relatedCell?: BaseCell;
18
+ align?: string | null;
19
+ }
20
+ export interface TH extends BaseCell {
21
+ type: 'th';
22
+ }
23
+ export interface TD extends BaseCell {
24
+ type: 'td';
25
+ }
26
+ export interface THeadRow {
27
+ type: 'tr';
28
+ tokens: TH[];
29
+ }
30
+ export interface TRow {
31
+ type: 'tr';
32
+ tokens: TD[];
33
+ }
34
+ export interface THead {
35
+ type: 'thead';
36
+ tokens: THeadRow[];
37
+ }
38
+ export interface TBody {
39
+ type: 'tbody';
40
+ tokens: TRow[];
41
+ }
42
+ export interface TFoot {
43
+ type: 'tfoot';
44
+ tokens: TRow[];
45
+ }
46
+ export type TableSection = THead | TBody | TFoot;
47
+ export interface TableToken {
48
+ type: 'table';
49
+ tokens: TableSection[];
50
+ raw: string;
51
+ }
52
+ interface WorkingCell extends BaseCell {
53
+ }
54
+ type WorkingRow = WorkingCell[];
55
+ export declare const DEFAULT_OPTIONS: Required<SpanTableOptions>;
56
+ export declare const getTableCell: (text: string, cell: BaseCell, type: "th" | "td", align: string | null) => string;
57
+ export declare const splitCells: (tableRow: string, count: number | null, prevRow?: WorkingRow | null, maxColspan?: number | null) => WorkingRow;
58
+ export declare function markedTable(options?: SpanTableOptions): {
59
+ extensions: {
60
+ name: string;
61
+ level: 'block' | 'inline';
62
+ start: (src: string) => number | undefined;
63
+ tokenizer: TokenizerExtensionFunction;
64
+ }[];
65
+ };
66
+ export {};