@kushagradhawan/kookie-blocks 0.1.10 → 0.1.11

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 (55) hide show
  1. package/dist/cjs/components/index.d.ts +1 -0
  2. package/dist/cjs/components/index.d.ts.map +1 -1
  3. package/dist/cjs/components/index.js +1 -1
  4. package/dist/cjs/components/index.js.map +2 -2
  5. package/dist/cjs/components/markdown/StreamingMarkdown.d.ts +78 -0
  6. package/dist/cjs/components/markdown/StreamingMarkdown.d.ts.map +1 -0
  7. package/dist/cjs/components/markdown/StreamingMarkdown.js +2 -0
  8. package/dist/cjs/components/markdown/StreamingMarkdown.js.map +7 -0
  9. package/dist/cjs/components/markdown/createMarkdownComponents.d.ts +27 -0
  10. package/dist/cjs/components/markdown/createMarkdownComponents.d.ts.map +1 -0
  11. package/dist/cjs/components/markdown/createMarkdownComponents.js +3 -0
  12. package/dist/cjs/components/markdown/createMarkdownComponents.js.map +7 -0
  13. package/dist/cjs/components/markdown/index.d.ts +6 -0
  14. package/dist/cjs/components/markdown/index.d.ts.map +1 -0
  15. package/dist/cjs/components/markdown/index.js +2 -0
  16. package/dist/cjs/components/markdown/index.js.map +7 -0
  17. package/dist/cjs/components/markdown/types.d.ts +32 -0
  18. package/dist/cjs/components/markdown/types.d.ts.map +1 -0
  19. package/dist/cjs/components/markdown/types.js +2 -0
  20. package/dist/cjs/components/markdown/types.js.map +7 -0
  21. package/dist/cjs/components/markdown/utils/markdownStreaming.d.ts +32 -0
  22. package/dist/cjs/components/markdown/utils/markdownStreaming.d.ts.map +1 -0
  23. package/dist/cjs/components/markdown/utils/markdownStreaming.js +5 -0
  24. package/dist/cjs/components/markdown/utils/markdownStreaming.js.map +7 -0
  25. package/dist/esm/components/index.d.ts +1 -0
  26. package/dist/esm/components/index.d.ts.map +1 -1
  27. package/dist/esm/components/index.js +1 -1
  28. package/dist/esm/components/index.js.map +2 -2
  29. package/dist/esm/components/markdown/StreamingMarkdown.d.ts +78 -0
  30. package/dist/esm/components/markdown/StreamingMarkdown.d.ts.map +1 -0
  31. package/dist/esm/components/markdown/StreamingMarkdown.js +2 -0
  32. package/dist/esm/components/markdown/StreamingMarkdown.js.map +7 -0
  33. package/dist/esm/components/markdown/createMarkdownComponents.d.ts +27 -0
  34. package/dist/esm/components/markdown/createMarkdownComponents.d.ts.map +1 -0
  35. package/dist/esm/components/markdown/createMarkdownComponents.js +3 -0
  36. package/dist/esm/components/markdown/createMarkdownComponents.js.map +7 -0
  37. package/dist/esm/components/markdown/index.d.ts +6 -0
  38. package/dist/esm/components/markdown/index.d.ts.map +1 -0
  39. package/dist/esm/components/markdown/index.js +2 -0
  40. package/dist/esm/components/markdown/index.js.map +7 -0
  41. package/dist/esm/components/markdown/types.d.ts +32 -0
  42. package/dist/esm/components/markdown/types.d.ts.map +1 -0
  43. package/dist/esm/components/markdown/types.js +1 -0
  44. package/dist/esm/components/markdown/types.js.map +7 -0
  45. package/dist/esm/components/markdown/utils/markdownStreaming.d.ts +32 -0
  46. package/dist/esm/components/markdown/utils/markdownStreaming.d.ts.map +1 -0
  47. package/dist/esm/components/markdown/utils/markdownStreaming.js +5 -0
  48. package/dist/esm/components/markdown/utils/markdownStreaming.js.map +7 -0
  49. package/package.json +10 -1
  50. package/src/components/index.ts +1 -0
  51. package/src/components/markdown/StreamingMarkdown.tsx +183 -0
  52. package/src/components/markdown/createMarkdownComponents.tsx +206 -0
  53. package/src/components/markdown/index.ts +8 -0
  54. package/src/components/markdown/types.ts +31 -0
  55. package/src/components/markdown/utils/markdownStreaming.ts +297 -0
@@ -0,0 +1,31 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * Options for customizing markdown component behavior
5
+ */
6
+ export type MarkdownComponentOptions = {
7
+ /**
8
+ * Whether code blocks should be collapsible
9
+ * @default false
10
+ */
11
+ codeBlockCollapsible?: boolean;
12
+
13
+ /**
14
+ * Custom image component
15
+ */
16
+ imageComponent?: (props: { src?: string; alt?: string; width?: string; height?: string }) => ReactNode;
17
+
18
+ /**
19
+ * Whether to use high contrast for inline code
20
+ * @default true
21
+ */
22
+ inlineCodeHighContrast?: boolean;
23
+ };
24
+
25
+ /**
26
+ * Common props for markdown child components
27
+ */
28
+ export type MarkdownChildrenProps = {
29
+ children?: ReactNode;
30
+ };
31
+
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Utilities for handling streaming markdown content.
3
+ * Completes unterminated markdown blocks to enable proper parsing during streaming.
4
+ */
5
+
6
+ /**
7
+ * Checks if there's an unpaired double marker (** or __) with content after the last opener.
8
+ * Returns true only if there's text after the unclosed marker (not just the marker itself).
9
+ */
10
+ function hasUnpairedDoubleMarkerWithContent(content: string, marker: string): boolean {
11
+ const escaped = marker === "**" ? "\\*\\*" : "__";
12
+ const regex = new RegExp(escaped, "g");
13
+ const matches = content.match(regex) || [];
14
+
15
+ if (matches.length % 2 === 0) {
16
+ return false; // All paired
17
+ }
18
+
19
+ // Find the last occurrence of the marker - there should be content after it
20
+ const lastIndex = content.lastIndexOf(marker);
21
+ if (lastIndex === -1) {
22
+ return false;
23
+ }
24
+
25
+ // Check if there's actual content after the marker
26
+ const afterMarker = content.slice(lastIndex + marker.length);
27
+ return afterMarker.length > 0;
28
+ }
29
+
30
+ /**
31
+ * Checks if there's an unpaired strikethrough with content after the last opener.
32
+ */
33
+ function hasUnpairedStrikethroughWithContent(content: string): boolean {
34
+ const matches = content.match(/~~/g) || [];
35
+
36
+ if (matches.length % 2 === 0) {
37
+ return false; // All paired
38
+ }
39
+
40
+ // Find the last occurrence - there should be content after it
41
+ const lastIndex = content.lastIndexOf("~~");
42
+ if (lastIndex === -1) {
43
+ return false;
44
+ }
45
+
46
+ const afterMarker = content.slice(lastIndex + 2);
47
+ return afterMarker.length > 0;
48
+ }
49
+
50
+ /**
51
+ * Checks if a marker at position i is a list marker (at start of line followed by space).
52
+ */
53
+ function isListMarker(content: string, i: number, marker: string): boolean {
54
+ // Only * can be a list marker (not _)
55
+ if (marker !== "*") {
56
+ return false;
57
+ }
58
+
59
+ // Check if at start of line (or start of content)
60
+ const isStartOfLine = i === 0 || content[i - 1] === "\n";
61
+ // Check if followed by space
62
+ const followedBySpace = i < content.length - 1 && content[i + 1] === " ";
63
+
64
+ return isStartOfLine && followedBySpace;
65
+ }
66
+
67
+ /**
68
+ * Checks if there's an unpaired single italic marker (* or _) that needs closing.
69
+ * Must distinguish from bold markers (** or __) and list markers (* ).
70
+ *
71
+ * Returns true if:
72
+ * - There's an odd number of single markers (not part of double markers or list markers)
73
+ * - Either there's content after the last marker, OR the content ends with the marker
74
+ */
75
+ function hasUnpairedItalicWithContent(content: string, marker: string): boolean {
76
+ // Count single markers (not part of double markers or list markers)
77
+ let singleCount = 0;
78
+ let lastSingleIndex = -1;
79
+
80
+ for (let i = 0; i < content.length; i++) {
81
+ if (content[i] === marker) {
82
+ // Check if it's a list marker
83
+ if (isListMarker(content, i, marker)) {
84
+ continue;
85
+ }
86
+
87
+ // Check if it's part of a double marker
88
+ const prevIsMarker = i > 0 && content[i - 1] === marker;
89
+ const nextIsMarker = i < content.length - 1 && content[i + 1] === marker;
90
+
91
+ if (prevIsMarker || nextIsMarker) {
92
+ // Part of ** or __, skip
93
+ if (nextIsMarker) {
94
+ i++; // Skip the next one too
95
+ }
96
+ continue;
97
+ }
98
+
99
+ singleCount++;
100
+ lastSingleIndex = i;
101
+ }
102
+ }
103
+
104
+ if (singleCount % 2 === 0) {
105
+ return false; // All paired
106
+ }
107
+
108
+ if (lastSingleIndex === -1) {
109
+ return false;
110
+ }
111
+
112
+ // If content ends with the marker, it's an opener that needs closing
113
+ // (e.g., "*text* more *" - the last * is a new opener)
114
+ // BUT only if there's content before it (not just a lone marker)
115
+ if (lastSingleIndex === content.length - 1) {
116
+ // Must have content before the marker for it to be a meaningful opener
117
+ return lastSingleIndex > 0;
118
+ }
119
+
120
+ // Otherwise, check if there's content after the last unpaired marker
121
+ const afterMarker = content.slice(lastSingleIndex + 1);
122
+ return afterMarker.length > 0;
123
+ }
124
+
125
+ /**
126
+ * Checks if content has an unclosed code block.
127
+ * Returns true if the last ``` opens a block that isn't closed.
128
+ */
129
+ function hasUnclosedCodeBlock(content: string): boolean {
130
+ // Find all ``` occurrences
131
+ const fenceRegex = /```/g;
132
+ const matches: number[] = [];
133
+ let match;
134
+
135
+ while ((match = fenceRegex.exec(content)) !== null) {
136
+ matches.push(match.index);
137
+ }
138
+
139
+ // Odd number of fences means unclosed
140
+ return matches.length % 2 === 1;
141
+ }
142
+
143
+ /**
144
+ * Completes unterminated markdown syntax at the end of content.
145
+ * This allows streaming markdown to be parsed correctly even when syntax is incomplete.
146
+ *
147
+ * Handles:
148
+ * - Headings (# Heading)
149
+ * - Inline code (`code)
150
+ * - Bold (**text or __text)
151
+ * - Italic (*text or _text)
152
+ * - Links ([text](url or [text]()
153
+ * - Code blocks (```language\ncode)
154
+ * - Lists (- item or * item or 1. item)
155
+ * - Blockquotes (> text)
156
+ * - Strikethrough (~~text)
157
+ */
158
+ export function completeUnterminatedMarkdown(content: string): string {
159
+ if (!content.trim()) {
160
+ return content;
161
+ }
162
+
163
+ // Work backwards from the end to find the last incomplete markdown pattern
164
+ const trimmed = content.trimEnd();
165
+ const trailingWhitespace = content.slice(trimmed.length);
166
+ let result = trimmed;
167
+
168
+ // Check for incomplete code fence without newline yet (e.g., "```python" with no \n)
169
+ // This must come before the code block check
170
+ const incompleteFenceMatch = result.match(/```[\w-]*$/);
171
+ if (incompleteFenceMatch && !result.includes("```\n") && !result.match(/```[\w-]+\n/)) {
172
+ // Code fence just started, hasn't gotten content yet - add newline and closing
173
+ result += "\n```";
174
+ return result + trailingWhitespace;
175
+ }
176
+
177
+ // Check for incomplete code blocks using fence counting
178
+ if (hasUnclosedCodeBlock(result)) {
179
+ result += "\n```";
180
+ return result + trailingWhitespace;
181
+ }
182
+
183
+ // Check for incomplete inline code (backticks)
184
+ // Count backticks that are NOT part of code fences
185
+ // First, remove all code fence markers to count only inline backticks
186
+ const withoutFences = result.replace(/```[\w-]*/g, "");
187
+ const backtickCount = (withoutFences.match(/`/g) || []).length;
188
+ if (backtickCount % 2 === 1) {
189
+ result += "`";
190
+ return result + trailingWhitespace;
191
+ }
192
+
193
+ // Check for incomplete links [text](url
194
+ const linkMatch = result.match(/\[([^\]]*)\]\(([^)]*)$/);
195
+ if (linkMatch) {
196
+ // Incomplete link - close the URL part
197
+ result += ")";
198
+ return result + trailingWhitespace;
199
+ }
200
+
201
+ // Check for incomplete bold (**text or __text)
202
+ // Only complete if there's actual content after the marker
203
+ if (hasUnpairedDoubleMarkerWithContent(result, "**")) {
204
+ result += "**";
205
+ return result + trailingWhitespace;
206
+ }
207
+
208
+ if (hasUnpairedDoubleMarkerWithContent(result, "__")) {
209
+ result += "__";
210
+ return result + trailingWhitespace;
211
+ }
212
+
213
+ // Check for incomplete strikethrough (~~text)
214
+ if (hasUnpairedStrikethroughWithContent(result)) {
215
+ result += "~~";
216
+ return result + trailingWhitespace;
217
+ }
218
+
219
+ // Check structural elements BEFORE italic to avoid false positives
220
+ // (e.g., "* item" is a list, not italic)
221
+ const lines = result.split("\n");
222
+ const lastLine = lines[lines.length - 1];
223
+
224
+ // Check for incomplete headings (# Heading without newline)
225
+ // Look for # at the start of the last line
226
+ if (lastLine.match(/^#{1,6}\s+.+$/)) {
227
+ // Heading without trailing newline - add one for proper parsing
228
+ result += "\n";
229
+ return result + trailingWhitespace;
230
+ }
231
+
232
+ // Check for incomplete list items (- item, * item, 1. item)
233
+ // Look for list markers at the start of the last line
234
+ if (lastLine.match(/^(\s*)([-*+]|\d+\.)\s+.+$/)) {
235
+ // List item without trailing newline - add one for proper parsing
236
+ result += "\n";
237
+ return result + trailingWhitespace;
238
+ }
239
+
240
+ // Check for incomplete blockquotes (> text)
241
+ if (lastLine.match(/^>\s+.+$/)) {
242
+ // Blockquote without trailing newline - add one for proper parsing
243
+ result += "\n";
244
+ return result + trailingWhitespace;
245
+ }
246
+
247
+ // Check for incomplete italic (*text or _text) AFTER structural elements
248
+ // This prevents "* item" from being treated as italic
249
+ if (hasUnpairedItalicWithContent(result, "*")) {
250
+ result += "*";
251
+ return result + trailingWhitespace;
252
+ }
253
+
254
+ if (hasUnpairedItalicWithContent(result, "_")) {
255
+ result += "_";
256
+ return result + trailingWhitespace;
257
+ }
258
+
259
+ return result + trailingWhitespace;
260
+ }
261
+
262
+ /**
263
+ * Parses markdown content into blocks for efficient rendering.
264
+ * Each block is a separate markdown token that can be memoized independently.
265
+ *
266
+ * @param content - The markdown content to parse
267
+ * @param parser - Optional parser function (defaults to using marked.lexer)
268
+ * @returns Array of markdown block strings
269
+ */
270
+ export function parseMarkdownIntoBlocks(
271
+ content: string,
272
+ parser?: (content: string) => Array<{ raw?: string }>
273
+ ): string[] {
274
+ if (!content.trim()) {
275
+ return [];
276
+ }
277
+
278
+ // Complete unterminated markdown blocks for streaming support
279
+ const completedContent = completeUnterminatedMarkdown(content);
280
+
281
+ // If no parser provided, return the content as a single block
282
+ // This allows usage without marked dependency
283
+ if (!parser) {
284
+ return [completedContent];
285
+ }
286
+
287
+ const tokens = parser(completedContent);
288
+ return tokens
289
+ .map((token) => {
290
+ if ("raw" in token && typeof token.raw === "string") {
291
+ return token.raw;
292
+ }
293
+ return "";
294
+ })
295
+ .filter((raw) => Boolean(raw.trim()));
296
+ }
297
+