svelte-streamdown 2.5.2 → 2.6.0
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 +105 -1
- package/dist/Elements/Element.svelte +12 -0
- package/dist/marked/index.d.ts +3 -2
- package/dist/marked/index.js +3 -2
- package/dist/marked/marked-mdx.d.ts +12 -0
- package/dist/marked/marked-mdx.js +124 -0
- package/dist/utils/parse-incomplete-markdown.js +121 -0
- package/dist/utils/url.js +0 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,6 +63,7 @@ Full support for
|
|
|
63
63
|
- Complex tables
|
|
64
64
|
- Footnotes [^1]
|
|
65
65
|
- Inline citations [ref] [ref2]
|
|
66
|
+
- MDX components (embed custom Svelte components)
|
|
66
67
|
|
|
67
68
|
[^1]:
|
|
68
69
|
Reference render in a popover by default.
|
|
@@ -489,7 +490,7 @@ This heading will use a custom component!`;
|
|
|
489
490
|
| `animation.tokenize` | `'word' \| 'char'` | `'word'` | Tokenization method for text animations |
|
|
490
491
|
| `animation.animateOnMount` | `boolean` | `false` | Run the token animation on mount or not, useful if you render the Streamdown component in the same time as the first token is receive from the LLM |
|
|
491
492
|
| `extensions` | `Array<Extension>` | `[]` | Custom marked tokenizers to render special markdown blocks or inline tokens |
|
|
492
|
-
| `children` | `Snippet<[{token:GenericToken, streamdown: StreamdownContext, children: Snippet` | `undefined` | Snippet used to render
|
|
493
|
+
| `children` | `Snippet<[{token:GenericToken, streamdown: StreamdownContext, children: Snippet` | `undefined` | Snippet used to render elements not supported by Streamdown, custom extensions, and MDX components |
|
|
493
494
|
|
|
494
495
|
#### All Available Customizable Elements:
|
|
495
496
|
|
|
@@ -505,6 +506,8 @@ This heading will use a custom component!`;
|
|
|
505
506
|
|
|
506
507
|
**Special Content**: `blockquote`, `hr`, `alert`, `mermaid`, `math`, `footnoteRef`, `inlineCitation`
|
|
507
508
|
|
|
509
|
+
**MDX Components**: Any PascalCase component (e.g., `Card`, `Button`, `MyComponent`) - pass as snippets with the component name
|
|
510
|
+
|
|
508
511
|
**Note**: The above elements are **supported by Streamdown** and should be customized using individual props or the theme system.
|
|
509
512
|
|
|
510
513
|
## 🎨 Theming System
|
|
@@ -612,6 +615,107 @@ Each component supports multiple themeable parts:
|
|
|
612
615
|
|
|
613
616
|
Themes are intelligently merged using Tailwind's class merging utility, so you only need to override the specific parts you want to customize while keeping the default styling for everything else.
|
|
614
617
|
|
|
618
|
+
## 🧩 MDX Component Support
|
|
619
|
+
|
|
620
|
+
Streamdown supports MDX-style JSX components, allowing you to embed custom Svelte components directly in your markdown content.
|
|
621
|
+
|
|
622
|
+
### Basic Usage
|
|
623
|
+
|
|
624
|
+
```svelte
|
|
625
|
+
<script>
|
|
626
|
+
import { Streamdown } from 'svelte-streamdown';
|
|
627
|
+
|
|
628
|
+
let content = `
|
|
629
|
+
# Using MDX Components
|
|
630
|
+
|
|
631
|
+
<Card title="Hello" count={42}>
|
|
632
|
+
This is **markdown content** inside a component!
|
|
633
|
+
</Card>
|
|
634
|
+
|
|
635
|
+
<Button label="Click me" active={true} />
|
|
636
|
+
`;
|
|
637
|
+
</script>
|
|
638
|
+
|
|
639
|
+
<Streamdown {content}>
|
|
640
|
+
{#snippet Card({ title, count, children })}
|
|
641
|
+
<div class="rounded-lg border border-gray-200 p-4 shadow-sm">
|
|
642
|
+
<h3 class="text-xl font-bold">{title}</h3>
|
|
643
|
+
<p class="text-gray-600">Count: {count}</p>
|
|
644
|
+
<div class="mt-2">
|
|
645
|
+
{@render children()}
|
|
646
|
+
</div>
|
|
647
|
+
</div>
|
|
648
|
+
{/snippet}
|
|
649
|
+
|
|
650
|
+
{#snippet Button({ label, active })}
|
|
651
|
+
<button class="rounded px-4 py-2 {active ? 'bg-blue-500 text-white' : 'bg-gray-200'}">
|
|
652
|
+
{label}
|
|
653
|
+
</button>
|
|
654
|
+
{/snippet}
|
|
655
|
+
</Streamdown>
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### Supported Syntax
|
|
659
|
+
|
|
660
|
+
**Self-closing components:**
|
|
661
|
+
```markdown
|
|
662
|
+
<Component attr="value" count={42} enabled={true} />
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
**Components with markdown children:**
|
|
666
|
+
```markdown
|
|
667
|
+
<Component title="Hello">
|
|
668
|
+
# This is a heading
|
|
669
|
+
This **markdown** content will be parsed!
|
|
670
|
+
</Component>
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Attribute Types
|
|
674
|
+
|
|
675
|
+
MDX components support three attribute value types:
|
|
676
|
+
|
|
677
|
+
- **Strings**: `attr="hello"` → `"hello"`
|
|
678
|
+
- **Numbers**: `count={42}` or `value={3.14}` → `42`, `3.14`
|
|
679
|
+
- **Booleans**: `active={true}` or `disabled={false}` → `true`, `false`
|
|
680
|
+
- **Expressions**: `value={variableName}` → `"variableName"` (stored as string)
|
|
681
|
+
|
|
682
|
+
### Component Naming
|
|
683
|
+
|
|
684
|
+
- Component names **must start with a capital letter** (PascalCase)
|
|
685
|
+
- Valid: `<Card />`, `<MyComponent />`, `<Component123 />`
|
|
686
|
+
- Invalid: `<card />`, `<myComponent />` (these are treated as HTML)
|
|
687
|
+
|
|
688
|
+
### Streaming Safety
|
|
689
|
+
|
|
690
|
+
MDX components are streaming-safe. Incomplete components are automatically handled during AI streaming:
|
|
691
|
+
|
|
692
|
+
- Incomplete tags like `<Component attr` are escaped with backticks
|
|
693
|
+
- Unclosed components like `<Card>content` are auto-closed with `</Card>`
|
|
694
|
+
- Malformed attributes are escaped to prevent rendering errors
|
|
695
|
+
|
|
696
|
+
This ensures your UI remains stable even when receiving partial markdown from streaming AI responses.
|
|
697
|
+
|
|
698
|
+
### Component Props
|
|
699
|
+
|
|
700
|
+
MDX component snippets receive:
|
|
701
|
+
- `token`: The full MdxToken with `tagName`, `attributes`, etc.
|
|
702
|
+
- `children`: Snippet containing parsed markdown content
|
|
703
|
+
- **All attributes spread directly**: Access attributes by name (e.g., `title`, `count`, `active`)
|
|
704
|
+
|
|
705
|
+
Example:
|
|
706
|
+
```svelte
|
|
707
|
+
<!-- Markdown: <Card title="Hello" count={5}>Content</Card> -->
|
|
708
|
+
<Streamdown {content}>
|
|
709
|
+
{#snippet Card({ title, count, children })}
|
|
710
|
+
<div>
|
|
711
|
+
<h3>{title}</h3>
|
|
712
|
+
<span>Count: {count}</span>
|
|
713
|
+
{@render children()}
|
|
714
|
+
</div>
|
|
715
|
+
{/snippet}
|
|
716
|
+
</Streamdown>
|
|
717
|
+
```
|
|
718
|
+
|
|
615
719
|
## 💉 Extensibility
|
|
616
720
|
|
|
617
721
|
Streamdown is extensible through the use of custom extensions.
|
|
@@ -298,6 +298,18 @@
|
|
|
298
298
|
typeof streamdown.renderHtml === 'function' ? streamdown.renderHtml(token) : token.raw}
|
|
299
299
|
{@html content}
|
|
300
300
|
{/if}
|
|
301
|
+
{:else if token.type === 'mdx'}
|
|
302
|
+
{#if token.tagName in streamdown.snippets}
|
|
303
|
+
<Slot
|
|
304
|
+
props={{ token, children, ...token.attributes }}
|
|
305
|
+
render={streamdown.snippets[token.tagName as keyof typeof streamdown.snippets]}
|
|
306
|
+
>
|
|
307
|
+
{@render children()}
|
|
308
|
+
</Slot>
|
|
309
|
+
{:else}
|
|
310
|
+
<!-- Fallback if no snippet provided for this component -->
|
|
311
|
+
{@render children()}
|
|
312
|
+
{/if}
|
|
301
313
|
{:else}
|
|
302
314
|
<!-- For tokens we don't handle specifically, it may certainely be a custom extension to to the children props to handle -->
|
|
303
315
|
{@render streamdown.children?.({ token, children, streamdown })}
|
package/dist/marked/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { type TableToken, type THead, type TBody, type TFoot, type THeadRow, typ
|
|
|
10
10
|
import { type DescriptionDetailToken, type DescriptionListToken, type DescriptionTermToken, type DescriptionToken } from './marked-dl.js';
|
|
11
11
|
import { type AlignToken } from './marked-align.js';
|
|
12
12
|
import { type CitationToken } from './marked-citations.js';
|
|
13
|
+
import { type MdxToken } from './marked-mdx.js';
|
|
13
14
|
export type GenericToken = {
|
|
14
15
|
type: string;
|
|
15
16
|
raw: string;
|
|
@@ -22,8 +23,8 @@ export type Extension = {
|
|
|
22
23
|
start?: TokenizerStartFunction;
|
|
23
24
|
applyInBlockParsing?: boolean;
|
|
24
25
|
};
|
|
25
|
-
export type StreamdownToken = Exclude<MarkedToken, Tokens.List | Tokens.ListItem | Tokens.Table> | ListToken | ListItemToken | MathToken | AlertToken | FootnoteToken | SubSupToken | BrToken | HrToken | TableToken | THead | TBody | TFoot | THeadRow | TRow | TH | TD | DescriptionListToken | DescriptionToken | DescriptionDetailToken | DescriptionTermToken | AlignToken | CitationToken;
|
|
26
|
+
export type StreamdownToken = Exclude<MarkedToken, Tokens.List | Tokens.ListItem | Tokens.Table> | ListToken | ListItemToken | MathToken | AlertToken | FootnoteToken | SubSupToken | BrToken | HrToken | TableToken | THead | TBody | TFoot | THeadRow | TRow | TH | TD | DescriptionListToken | DescriptionToken | DescriptionDetailToken | DescriptionTermToken | AlignToken | CitationToken | MdxToken;
|
|
26
27
|
export type { TableToken, THead, TBody, TFoot, THeadRow, TRow, TH, TD } from './marked-table.js';
|
|
27
28
|
export declare const lex: (markdown: string, extensions?: Extension[]) => StreamdownToken[];
|
|
28
29
|
export declare const parseBlocks: (markdown: string, extensions?: Extension[]) => string[];
|
|
29
|
-
export type { MathToken, AlertToken, FootnoteToken, SubSupToken, BrToken, HrToken, AlignToken, CitationToken };
|
|
30
|
+
export type { MathToken, AlertToken, FootnoteToken, SubSupToken, BrToken, HrToken, AlignToken, CitationToken, MdxToken };
|
package/dist/marked/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { markedTable } from './marked-table.js';
|
|
|
10
10
|
import { markedDl } from './marked-dl.js';
|
|
11
11
|
import { markedAlign } from './marked-align.js';
|
|
12
12
|
import { markedCitations } from './marked-citations.js';
|
|
13
|
+
import { markedMdx } from './marked-mdx.js';
|
|
13
14
|
const parseExtensions = (...extensions) => {
|
|
14
15
|
const options = {
|
|
15
16
|
gfm: true,
|
|
@@ -43,12 +44,12 @@ const parseExtensions = (...extensions) => {
|
|
|
43
44
|
return options;
|
|
44
45
|
};
|
|
45
46
|
export const lex = (markdown, extensions = []) => {
|
|
46
|
-
return new Lexer(parseExtensions(markedHr, markedTable, ...markedFootnote(), markedAlert, ...markedMath, markedSub, markedSup, markedList, markedBr, markedDl, markedAlign, markedCitations, ...extensions))
|
|
47
|
+
return new Lexer(parseExtensions(markedHr, markedTable, ...markedFootnote(), markedAlert, ...markedMath, markedSub, markedSup, markedList, markedBr, markedDl, markedAlign, markedCitations, markedMdx, ...extensions))
|
|
47
48
|
.lex(markdown)
|
|
48
49
|
.filter((token) => token.type !== 'space' && token.type !== 'footnote');
|
|
49
50
|
};
|
|
50
51
|
export const parseBlocks = (markdown, extensions = []) => {
|
|
51
|
-
const blockLexer = new Lexer(parseExtensions(markedHr, ...markedFootnote(), markedDl, markedTable, markedAlign, ...extensions.filter(({ level, applyInBlockParsing }) => level === 'block' && applyInBlockParsing)));
|
|
52
|
+
const blockLexer = new Lexer(parseExtensions(markedHr, ...markedFootnote(), markedDl, markedTable, markedAlign, markedMdx, ...extensions.filter(({ level, applyInBlockParsing }) => level === 'block' && applyInBlockParsing)));
|
|
52
53
|
return blockLexer.blockTokens(markdown, []).reduce((acc, block) => {
|
|
53
54
|
if (block.type === 'space' || block.type === 'footnote') {
|
|
54
55
|
return acc;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Extension } from './index.js';
|
|
2
|
+
import type { Token } from 'marked';
|
|
3
|
+
export type MdxToken = {
|
|
4
|
+
type: 'mdx';
|
|
5
|
+
raw: string;
|
|
6
|
+
tagName: string;
|
|
7
|
+
attributes: Record<string, any>;
|
|
8
|
+
selfClosing: boolean;
|
|
9
|
+
tokens?: Token[];
|
|
10
|
+
text?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare const markedMdx: Extension;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Lexer } from 'marked';
|
|
2
|
+
const defaultLexer = new Lexer({ gfm: true });
|
|
3
|
+
const defaultTokenizer = defaultLexer.options.tokenizer;
|
|
4
|
+
/**
|
|
5
|
+
* Parse attributes from MDX component tag
|
|
6
|
+
* Supports: attribute="string", attribute={number}, attribute={boolean}, attribute={expression}
|
|
7
|
+
*/
|
|
8
|
+
function parseAttributes(attributeString) {
|
|
9
|
+
const attributes = {};
|
|
10
|
+
// Pattern: attr="value" or attr={value}
|
|
11
|
+
const attrPattern = /(\w+)=(?:"([^"]*)"|{([^}]*)})/g;
|
|
12
|
+
let match;
|
|
13
|
+
while ((match = attrPattern.exec(attributeString)) !== null) {
|
|
14
|
+
const [, name, stringValue, expressionValue] = match;
|
|
15
|
+
if (stringValue !== undefined) {
|
|
16
|
+
// String attribute: attr="value"
|
|
17
|
+
attributes[name] = stringValue;
|
|
18
|
+
}
|
|
19
|
+
else if (expressionValue !== undefined) {
|
|
20
|
+
// Expression attribute: attr={value}
|
|
21
|
+
const trimmed = expressionValue.trim();
|
|
22
|
+
// Try to parse as number
|
|
23
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
24
|
+
attributes[name] = parseFloat(trimmed);
|
|
25
|
+
}
|
|
26
|
+
// Parse boolean
|
|
27
|
+
else if (trimmed === 'true') {
|
|
28
|
+
attributes[name] = true;
|
|
29
|
+
}
|
|
30
|
+
else if (trimmed === 'false') {
|
|
31
|
+
attributes[name] = false;
|
|
32
|
+
}
|
|
33
|
+
// Otherwise keep as string (could be variable reference, etc.)
|
|
34
|
+
else {
|
|
35
|
+
attributes[name] = trimmed;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return attributes;
|
|
40
|
+
}
|
|
41
|
+
export const markedMdx = {
|
|
42
|
+
name: 'mdx',
|
|
43
|
+
level: 'block',
|
|
44
|
+
applyInBlockParsing: true,
|
|
45
|
+
tokenizer(src) {
|
|
46
|
+
// Match MDX component tags (must start with capital letter)
|
|
47
|
+
// Self-closing: <Component attr="value" />
|
|
48
|
+
// With children: <Component attr="value">content</Component>
|
|
49
|
+
// First try self-closing tag
|
|
50
|
+
const selfClosingMatch = src.match(/^<([A-Z][a-zA-Z0-9]*)((?:\s+\w+=(?:"[^"]*"|{[^}]*}))*)\s*\/>/);
|
|
51
|
+
if (selfClosingMatch) {
|
|
52
|
+
const [raw, tagName, attributeString] = selfClosingMatch;
|
|
53
|
+
const attributes = parseAttributes(attributeString);
|
|
54
|
+
return {
|
|
55
|
+
type: 'mdx',
|
|
56
|
+
raw,
|
|
57
|
+
tagName,
|
|
58
|
+
attributes,
|
|
59
|
+
selfClosing: true
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// Try paired tag with children
|
|
63
|
+
const openTagMatch = src.match(/^<([A-Z][a-zA-Z0-9]*)((?:\s+\w+=(?:"[^"]*"|{[^}]*}))*)\s*>/);
|
|
64
|
+
if (openTagMatch) {
|
|
65
|
+
const [openTag, tagName, attributeString] = openTagMatch;
|
|
66
|
+
const attributes = parseAttributes(attributeString);
|
|
67
|
+
// Find matching closing tag with nesting support
|
|
68
|
+
const closingTag = `</${tagName}>`;
|
|
69
|
+
// Escape special regex characters in tagName to prevent ReDoS
|
|
70
|
+
const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
71
|
+
const openTagPattern = new RegExp(`<${escapedTagName}(?:\\s|>)`, 'g');
|
|
72
|
+
const closeTagPattern = new RegExp(`</${escapedTagName}>`, 'g');
|
|
73
|
+
let depth = 1;
|
|
74
|
+
let searchPos = openTag.length;
|
|
75
|
+
let closingIndex = -1;
|
|
76
|
+
while (depth > 0 && searchPos < src.length) {
|
|
77
|
+
openTagPattern.lastIndex = searchPos;
|
|
78
|
+
closeTagPattern.lastIndex = searchPos;
|
|
79
|
+
const nextOpen = openTagPattern.exec(src);
|
|
80
|
+
const nextClose = closeTagPattern.exec(src);
|
|
81
|
+
if (!nextClose)
|
|
82
|
+
break;
|
|
83
|
+
if (nextOpen && nextOpen.index < nextClose.index) {
|
|
84
|
+
// Verify this is not a self-closing tag by finding the full tag and checking for />
|
|
85
|
+
const tagStart = nextOpen.index;
|
|
86
|
+
const tagEndPos = src.indexOf('>', tagStart);
|
|
87
|
+
if (tagEndPos !== -1) {
|
|
88
|
+
const fullTag = src.substring(tagStart, tagEndPos + 1);
|
|
89
|
+
const isSelfClosing = fullTag.trimEnd().endsWith('/>');
|
|
90
|
+
if (!isSelfClosing) {
|
|
91
|
+
depth++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
searchPos = openTagPattern.lastIndex;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
depth--;
|
|
98
|
+
if (depth === 0) {
|
|
99
|
+
closingIndex = nextClose.index;
|
|
100
|
+
}
|
|
101
|
+
searchPos = closeTagPattern.lastIndex;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (closingIndex !== -1) {
|
|
105
|
+
// Extract content between tags
|
|
106
|
+
const contentStart = openTag.length;
|
|
107
|
+
const content = src.substring(contentStart, closingIndex);
|
|
108
|
+
const raw = src.substring(0, closingIndex + closingTag.length);
|
|
109
|
+
// Parse children as markdown
|
|
110
|
+
const tokens = content.trim() ? this.lexer.blockTokens(content.trim(), []) : [];
|
|
111
|
+
return {
|
|
112
|
+
type: 'mdx',
|
|
113
|
+
raw,
|
|
114
|
+
tagName,
|
|
115
|
+
attributes,
|
|
116
|
+
selfClosing: false,
|
|
117
|
+
tokens,
|
|
118
|
+
text: content
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
@@ -670,6 +670,127 @@ class IncompleteMarkdownParser {
|
|
|
670
670
|
}
|
|
671
671
|
return line;
|
|
672
672
|
}
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
name: 'mdx',
|
|
676
|
+
skipInBlockTypes: ['code', 'math', 'center', 'right'],
|
|
677
|
+
preprocess: ({ text }) => {
|
|
678
|
+
// Track MDX component states across the entire text
|
|
679
|
+
const lines = text.split('\n');
|
|
680
|
+
const openTags = [];
|
|
681
|
+
let mdxLineStates = [];
|
|
682
|
+
for (let i = 0; i < lines.length; i++) {
|
|
683
|
+
const line = lines[i];
|
|
684
|
+
let inMdx = false;
|
|
685
|
+
let incompletePositions = [];
|
|
686
|
+
// Find all MDX tags in the line
|
|
687
|
+
let searchPos = 0;
|
|
688
|
+
while (searchPos < line.length) {
|
|
689
|
+
// Look for opening bracket with capital letter (MDX component)
|
|
690
|
+
const tagStart = line.indexOf('<', searchPos);
|
|
691
|
+
if (tagStart === -1 || tagStart >= line.length - 1)
|
|
692
|
+
break;
|
|
693
|
+
const nextChar = line[tagStart + 1];
|
|
694
|
+
// Only match if starts with capital letter (MDX component)
|
|
695
|
+
if (!/[A-Z]/.test(nextChar)) {
|
|
696
|
+
searchPos = tagStart + 1;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
// Try to match complete self-closing tag
|
|
700
|
+
const selfClosingMatch = line
|
|
701
|
+
.substring(tagStart)
|
|
702
|
+
.match(/^<([A-Z][a-zA-Z0-9]*)((?:\s+\w+=(?:"[^"]*"|{[^}]*}))*)\s*\/>/);
|
|
703
|
+
if (selfClosingMatch) {
|
|
704
|
+
searchPos = tagStart + selfClosingMatch[0].length;
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
// Try to match complete opening tag with immediate closing
|
|
708
|
+
const completeMatch = line
|
|
709
|
+
.substring(tagStart)
|
|
710
|
+
.match(/^<([A-Z][a-zA-Z0-9]*)((?:\s+\w+=(?:"[^"]*"|{[^}]*}))*)\s*>.*?<\/\1>/);
|
|
711
|
+
if (completeMatch) {
|
|
712
|
+
searchPos = tagStart + completeMatch[0].length;
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
// Try to match opening tag
|
|
716
|
+
const openTagMatch = line
|
|
717
|
+
.substring(tagStart)
|
|
718
|
+
.match(/^<([A-Z][a-zA-Z0-9]*)((?:\s+\w+=(?:"[^"]*"|{[^}]*}))*)\s*>/);
|
|
719
|
+
if (openTagMatch) {
|
|
720
|
+
const tagName = openTagMatch[1];
|
|
721
|
+
openTags.push({ tagName, lineIndex: i });
|
|
722
|
+
inMdx = true;
|
|
723
|
+
searchPos = tagStart + openTagMatch[0].length;
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
// Check for incomplete self-closing (e.g., <Component /)
|
|
727
|
+
const incompleteSelfClosing = line
|
|
728
|
+
.substring(tagStart)
|
|
729
|
+
.match(/^<([A-Z][a-zA-Z0-9]*)[^>]*\/$/);
|
|
730
|
+
if (incompleteSelfClosing) {
|
|
731
|
+
incompletePositions.push(tagStart);
|
|
732
|
+
break; // This is at the end of the line
|
|
733
|
+
}
|
|
734
|
+
// Check for incomplete tag (no closing >) - only at end of line
|
|
735
|
+
const incompleteTag = line
|
|
736
|
+
.substring(tagStart)
|
|
737
|
+
.match(/^<([A-Z][a-zA-Z0-9]*)(?:\s+[^>]*)?$/);
|
|
738
|
+
if (incompleteTag) {
|
|
739
|
+
incompletePositions.push(tagStart);
|
|
740
|
+
break; // This is at the end of the line
|
|
741
|
+
}
|
|
742
|
+
searchPos = tagStart + 1;
|
|
743
|
+
}
|
|
744
|
+
// Check for closing tags
|
|
745
|
+
const closeTagMatches = line.matchAll(/<\/([A-Z][a-zA-Z0-9]*)>/g);
|
|
746
|
+
for (const closeMatch of closeTagMatches) {
|
|
747
|
+
const tagName = closeMatch[1];
|
|
748
|
+
// Find and remove the matching open tag
|
|
749
|
+
const openIndex = openTags.findIndex((t) => t.tagName === tagName);
|
|
750
|
+
if (openIndex !== -1) {
|
|
751
|
+
openTags.splice(openIndex, 1);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
mdxLineStates[i] = { inMdx, incompletePositions };
|
|
755
|
+
}
|
|
756
|
+
return {
|
|
757
|
+
text,
|
|
758
|
+
state: {
|
|
759
|
+
mdxUnclosedTags: openTags,
|
|
760
|
+
mdxLineStates
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
},
|
|
764
|
+
handler: ({ line, state }) => {
|
|
765
|
+
// Escape incomplete MDX syntax by wrapping in backticks
|
|
766
|
+
const lineStates = state.mdxLineStates || [];
|
|
767
|
+
const currentState = lineStates[state.currentLine];
|
|
768
|
+
if (currentState?.incompletePositions && currentState.incompletePositions.length > 0) {
|
|
769
|
+
// Process incomplete positions from right to left to preserve indices
|
|
770
|
+
let result = line;
|
|
771
|
+
for (let i = currentState.incompletePositions.length - 1; i >= 0; i--) {
|
|
772
|
+
const pos = currentState.incompletePositions[i];
|
|
773
|
+
const before = result.substring(0, pos);
|
|
774
|
+
const incomplete = result.substring(pos);
|
|
775
|
+
result = before + '`' + incomplete + '`';
|
|
776
|
+
}
|
|
777
|
+
return result;
|
|
778
|
+
}
|
|
779
|
+
return line;
|
|
780
|
+
},
|
|
781
|
+
postprocess: ({ text, state }) => {
|
|
782
|
+
// Complete unclosed MDX components at the end
|
|
783
|
+
const unclosedTags = state.mdxUnclosedTags || [];
|
|
784
|
+
if (unclosedTags.length > 0) {
|
|
785
|
+
// Close tags in reverse order (innermost first)
|
|
786
|
+
let result = text;
|
|
787
|
+
for (let i = unclosedTags.length - 1; i >= 0; i--) {
|
|
788
|
+
result += `\n</${unclosedTags[i].tagName}>`;
|
|
789
|
+
}
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
return text;
|
|
793
|
+
}
|
|
673
794
|
}
|
|
674
795
|
];
|
|
675
796
|
}
|
package/dist/utils/url.js
CHANGED
|
@@ -29,7 +29,6 @@ export const transformUrl = (url, allowedPrefixes, defaultOrigin) => {
|
|
|
29
29
|
if (!url)
|
|
30
30
|
return null;
|
|
31
31
|
const parsedUrl = parseUrl(url, defaultOrigin);
|
|
32
|
-
console.log('parsedUrl', { parsedUrl, url });
|
|
33
32
|
if (!parsedUrl)
|
|
34
33
|
return null;
|
|
35
34
|
// If the input is path relative, we output a path relative URL as well,
|