@changerawr/markdown 1.0.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.
@@ -0,0 +1,1752 @@
1
+ "use client";
2
+
3
+ // src/react/MarkdownRenderer.tsx
4
+ import React, { useMemo as useMemo2, useEffect as useEffect2 } from "react";
5
+
6
+ // src/react/hooks.ts
7
+ import { useState, useCallback, useMemo, useRef, useEffect } from "react";
8
+
9
+ // src/parser.ts
10
+ var MarkdownParser = class {
11
+ constructor(config) {
12
+ this.rules = [];
13
+ this.warnings = [];
14
+ this.config = config || {};
15
+ }
16
+ addRule(rule) {
17
+ this.rules.push(rule);
18
+ this.rules.sort((a, b) => {
19
+ if (a.name.includes("alert") || a.name.includes("embed") || a.name.includes("button")) return -1;
20
+ if (b.name.includes("alert") || b.name.includes("embed") || b.name.includes("button")) return 1;
21
+ if (a.name === "task-list") return -1;
22
+ if (b.name === "task-list") return 1;
23
+ if (a.name === "list" && b.name === "task-list") return 1;
24
+ if (b.name === "list" && a.name === "task-list") return -1;
25
+ return a.name.localeCompare(b.name);
26
+ });
27
+ }
28
+ setupDefaultRulesIfEmpty() {
29
+ const hasDefaultRules = this.rules.some(
30
+ (rule) => !["alert", "button", "embed"].includes(rule.name)
31
+ );
32
+ if (!hasDefaultRules) {
33
+ this.setupDefaultRules();
34
+ }
35
+ }
36
+ hasRule(name) {
37
+ return this.rules.some((rule) => rule.name === name);
38
+ }
39
+ parse(markdown2) {
40
+ this.warnings = [];
41
+ this.setupDefaultRulesIfEmpty();
42
+ const processedMarkdown = this.preprocessMarkdown(markdown2);
43
+ const tokens = [];
44
+ let remaining = processedMarkdown;
45
+ let iterationCount = 0;
46
+ const maxIterations = this.config.maxIterations || markdown2.length * 2;
47
+ while (remaining.length > 0 && iterationCount < maxIterations) {
48
+ iterationCount++;
49
+ let matched = false;
50
+ let bestMatch = null;
51
+ for (const rule of this.rules) {
52
+ try {
53
+ const pattern = new RegExp(rule.pattern.source, rule.pattern.flags.replace("g", ""));
54
+ const match = remaining.match(pattern);
55
+ if (match && match.index !== void 0) {
56
+ const priority = match.index === 0 ? 1e3 : 1e3 - match.index;
57
+ if (!bestMatch || priority > bestMatch.priority || priority === bestMatch.priority && match.index < (bestMatch.match.index || 0)) {
58
+ bestMatch = { rule, match, priority };
59
+ }
60
+ }
61
+ } catch (error) {
62
+ if (this.config.debugMode) {
63
+ console.warn(`Error in rule "${rule.name}":`, error);
64
+ }
65
+ }
66
+ }
67
+ if (bestMatch && bestMatch.match.index !== void 0) {
68
+ const { rule, match } = bestMatch;
69
+ const matchIndex = match.index;
70
+ if (matchIndex > 0) {
71
+ const textBefore = remaining.slice(0, matchIndex);
72
+ tokens.push({
73
+ type: "text",
74
+ content: textBefore,
75
+ raw: textBefore
76
+ });
77
+ remaining = remaining.slice(matchIndex);
78
+ continue;
79
+ }
80
+ try {
81
+ const token = rule.render(match);
82
+ tokens.push({
83
+ ...token,
84
+ raw: match[0] || ""
85
+ });
86
+ remaining = remaining.slice(match[0]?.length || 0);
87
+ matched = true;
88
+ } catch (error) {
89
+ const errorMessage = error instanceof Error ? error.message : String(error);
90
+ this.warnings.push(`Failed to render ${rule.name}: ${errorMessage}`);
91
+ const char = remaining[0];
92
+ if (char) {
93
+ tokens.push({
94
+ type: "text",
95
+ content: char,
96
+ raw: char
97
+ });
98
+ remaining = remaining.slice(1);
99
+ }
100
+ }
101
+ }
102
+ if (!matched) {
103
+ const char = remaining[0];
104
+ if (char) {
105
+ tokens.push({
106
+ type: "text",
107
+ content: char,
108
+ raw: char
109
+ });
110
+ remaining = remaining.slice(1);
111
+ }
112
+ }
113
+ }
114
+ if (iterationCount >= maxIterations) {
115
+ this.warnings.push("Parser hit maximum iterations - possible infinite loop detected");
116
+ }
117
+ const processedTokens = this.postProcessTokens(tokens);
118
+ return processedTokens;
119
+ }
120
+ getWarnings() {
121
+ return [...this.warnings];
122
+ }
123
+ getConfig() {
124
+ return { ...this.config };
125
+ }
126
+ updateConfig(config) {
127
+ this.config = { ...this.config, ...config };
128
+ }
129
+ setDebugMode(enabled) {
130
+ this.config.debugMode = enabled;
131
+ }
132
+ clearWarnings() {
133
+ this.warnings = [];
134
+ }
135
+ getIterationCount() {
136
+ return 0;
137
+ }
138
+ preprocessMarkdown(markdown2) {
139
+ if (this.config.validateMarkdown) {
140
+ this.validateMarkdown(markdown2);
141
+ }
142
+ return markdown2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
143
+ }
144
+ validateMarkdown(markdown2) {
145
+ const boldMatches = markdown2.match(/\*\*/g);
146
+ if (boldMatches && boldMatches.length % 2 !== 0) {
147
+ this.warnings.push("Unclosed bold markers (**) detected - some bold formatting may not work");
148
+ }
149
+ const italicMatches = markdown2.match(/(?<!\*)\*(?!\*)/g);
150
+ if (italicMatches && italicMatches.length % 2 !== 0) {
151
+ this.warnings.push("Unclosed italic markers (*) detected - some italic formatting may not work");
152
+ }
153
+ const codeBlockMatches = markdown2.match(/```/g);
154
+ if (codeBlockMatches && codeBlockMatches.length % 2 !== 0) {
155
+ this.warnings.push("Unclosed code blocks (```) detected - some code formatting may not work");
156
+ }
157
+ const inlineCodeMatches = markdown2.match(/`/g);
158
+ if (inlineCodeMatches && inlineCodeMatches.length % 2 !== 0) {
159
+ this.warnings.push("Unclosed inline code markers (`) detected - some code formatting may not work");
160
+ }
161
+ }
162
+ postProcessTokens(tokens) {
163
+ const processed = [];
164
+ let i = 0;
165
+ while (i < tokens.length) {
166
+ const token = tokens[i];
167
+ if (token && token.type === "text") {
168
+ let textContent = token.content || token.raw || "";
169
+ let j = i + 1;
170
+ while (j < tokens.length && tokens[j] && tokens[j].type === "text") {
171
+ const nextToken = tokens[j];
172
+ textContent += nextToken.content || nextToken.raw || "";
173
+ j++;
174
+ }
175
+ if (textContent.trim().length > 0 || textContent.includes("\n")) {
176
+ processed.push({
177
+ type: "text",
178
+ content: textContent,
179
+ raw: textContent
180
+ });
181
+ }
182
+ i = j;
183
+ } else if (token) {
184
+ processed.push(token);
185
+ i++;
186
+ } else {
187
+ i++;
188
+ }
189
+ }
190
+ return processed;
191
+ }
192
+ setupDefaultRules() {
193
+ this.addRule({
194
+ name: "heading",
195
+ pattern: /^(#{1,6})\s+(.+)$/m,
196
+ render: (match) => ({
197
+ type: "heading",
198
+ content: match[2]?.trim() || "",
199
+ raw: match[0] || "",
200
+ attributes: {
201
+ level: String(match[1]?.length || 1)
202
+ }
203
+ })
204
+ });
205
+ this.addRule({
206
+ name: "codeblock",
207
+ pattern: /```(\w+)?\s*\n([\s\S]*?)\n```/,
208
+ render: (match) => ({
209
+ type: "codeblock",
210
+ content: match[2] || "",
211
+ raw: match[0] || "",
212
+ attributes: {
213
+ language: match[1] || "text"
214
+ }
215
+ })
216
+ });
217
+ this.addRule({
218
+ name: "hard-break-backslash",
219
+ pattern: /\\\s*\n/,
220
+ render: (match) => ({
221
+ type: "line-break",
222
+ content: "",
223
+ raw: match[0] || ""
224
+ })
225
+ });
226
+ this.addRule({
227
+ name: "hard-break-spaces",
228
+ pattern: / +\n/,
229
+ render: (match) => ({
230
+ type: "line-break",
231
+ content: "",
232
+ raw: match[0] || ""
233
+ })
234
+ });
235
+ this.addRule({
236
+ name: "paragraph-break",
237
+ pattern: /\n\s*\n/,
238
+ render: (match) => ({
239
+ type: "paragraph-break",
240
+ content: "",
241
+ raw: match[0] || ""
242
+ })
243
+ });
244
+ this.addRule({
245
+ name: "bold",
246
+ pattern: /\*\*((?:(?!\*\*).)+)\*\*/,
247
+ render: (match) => ({
248
+ type: "bold",
249
+ content: match[1] || "",
250
+ raw: match[0] || ""
251
+ })
252
+ });
253
+ this.addRule({
254
+ name: "italic",
255
+ pattern: /\*((?:(?!\*).)+)\*/,
256
+ render: (match) => ({
257
+ type: "italic",
258
+ content: match[1] || "",
259
+ raw: match[0] || ""
260
+ })
261
+ });
262
+ this.addRule({
263
+ name: "code",
264
+ pattern: /`([^`]+)`/,
265
+ render: (match) => ({
266
+ type: "code",
267
+ content: match[1] || "",
268
+ raw: match[0] || ""
269
+ })
270
+ });
271
+ this.addRule({
272
+ name: "image",
273
+ pattern: /!\[([^\]]*)\]\(([^)]+?)(?:\s+"([^"]+)")?\)/,
274
+ render: (match) => ({
275
+ type: "image",
276
+ content: match[1] || "",
277
+ raw: match[0] || "",
278
+ attributes: {
279
+ alt: match[1] || "",
280
+ src: match[2] || "",
281
+ title: match[3] || ""
282
+ }
283
+ })
284
+ });
285
+ this.addRule({
286
+ name: "link",
287
+ pattern: /\[([^\]]+)\]\(([^)]+)\)/,
288
+ render: (match) => ({
289
+ type: "link",
290
+ content: match[1] || "",
291
+ raw: match[0] || "",
292
+ attributes: {
293
+ href: match[2] || ""
294
+ }
295
+ })
296
+ });
297
+ this.addRule({
298
+ name: "task-list",
299
+ pattern: /^(\s*)-\s*\[([ xX])\]\s*(.+)$/m,
300
+ render: (match) => ({
301
+ type: "task-item",
302
+ content: match[3] || "",
303
+ raw: match[0] || "",
304
+ attributes: {
305
+ checked: String((match[2] || "").toLowerCase() === "x")
306
+ }
307
+ })
308
+ });
309
+ this.addRule({
310
+ name: "list",
311
+ pattern: /^(\s*)[-*+]\s+(.+)$/m,
312
+ render: (match) => ({
313
+ type: "list-item",
314
+ content: match[2] || "",
315
+ raw: match[0] || ""
316
+ })
317
+ });
318
+ this.addRule({
319
+ name: "blockquote",
320
+ pattern: /^>\s+(.+)$/m,
321
+ render: (match) => ({
322
+ type: "blockquote",
323
+ content: match[1] || "",
324
+ raw: match[0] || ""
325
+ })
326
+ });
327
+ this.addRule({
328
+ name: "hr",
329
+ pattern: /^---$/m,
330
+ render: (match) => ({
331
+ type: "hr",
332
+ content: "",
333
+ raw: match[0] || ""
334
+ })
335
+ });
336
+ this.addRule({
337
+ name: "soft-break",
338
+ pattern: /\n/,
339
+ render: (match) => ({
340
+ type: "soft-break",
341
+ content: " ",
342
+ // Convert to space for inline text
343
+ raw: match[0] || ""
344
+ })
345
+ });
346
+ }
347
+ };
348
+
349
+ // src/utils.ts
350
+ var DOMPurify = {
351
+ sanitize: (html) => html
352
+ };
353
+ if (typeof window !== "undefined") {
354
+ import("dompurify").then((module) => {
355
+ DOMPurify = module.default;
356
+ }).catch((err) => {
357
+ console.error("Failed to load DOMPurify", err);
358
+ });
359
+ }
360
+ var ALLOWED_TAGS = [
361
+ // Standard HTML
362
+ "h1",
363
+ "h2",
364
+ "h3",
365
+ "h4",
366
+ "h5",
367
+ "h6",
368
+ "p",
369
+ "br",
370
+ "strong",
371
+ "em",
372
+ "del",
373
+ "ins",
374
+ "a",
375
+ "img",
376
+ "ul",
377
+ "ol",
378
+ "li",
379
+ "blockquote",
380
+ "pre",
381
+ "code",
382
+ "table",
383
+ "thead",
384
+ "tbody",
385
+ "tr",
386
+ "th",
387
+ "td",
388
+ "div",
389
+ "span",
390
+ "sup",
391
+ "sub",
392
+ "hr",
393
+ "input",
394
+ // Embeds
395
+ "iframe",
396
+ "embed",
397
+ "object",
398
+ "param",
399
+ "video",
400
+ "audio",
401
+ "source",
402
+ // SVG
403
+ "svg",
404
+ "path",
405
+ "polyline",
406
+ "line",
407
+ "circle",
408
+ "rect",
409
+ "g",
410
+ "defs",
411
+ "use",
412
+ // Form elements
413
+ "form",
414
+ "fieldset",
415
+ "legend",
416
+ "label",
417
+ "select",
418
+ "option",
419
+ "textarea",
420
+ "button"
421
+ ];
422
+ var ALLOWED_ATTR = [
423
+ // Standard attributes
424
+ "href",
425
+ "title",
426
+ "alt",
427
+ "src",
428
+ "class",
429
+ "id",
430
+ "target",
431
+ "rel",
432
+ "type",
433
+ "checked",
434
+ "disabled",
435
+ "loading",
436
+ "width",
437
+ "height",
438
+ "style",
439
+ "role",
440
+ // Iframe attributes
441
+ "frameborder",
442
+ "allowfullscreen",
443
+ "allow",
444
+ "sandbox",
445
+ "scrolling",
446
+ "allowtransparency",
447
+ "name",
448
+ "seamless",
449
+ "srcdoc",
450
+ // Data attributes (for embeds)
451
+ "data-*",
452
+ // SVG attributes
453
+ "viewBox",
454
+ "fill",
455
+ "stroke",
456
+ "stroke-width",
457
+ "stroke-linecap",
458
+ "stroke-linejoin",
459
+ "d",
460
+ "points",
461
+ "x1",
462
+ "y1",
463
+ "x2",
464
+ "y2",
465
+ "cx",
466
+ "cy",
467
+ "r",
468
+ "rx",
469
+ "ry",
470
+ // Media attributes
471
+ "autoplay",
472
+ "controls",
473
+ "loop",
474
+ "muted",
475
+ "preload",
476
+ "poster",
477
+ // Form attributes
478
+ "value",
479
+ "placeholder",
480
+ "required",
481
+ "readonly",
482
+ "maxlength",
483
+ "minlength",
484
+ "max",
485
+ "min",
486
+ "step",
487
+ "pattern",
488
+ "autocomplete",
489
+ "autofocus"
490
+ ];
491
+ function escapeHtml(text) {
492
+ const map = {
493
+ "&": "&amp;",
494
+ "<": "&lt;",
495
+ ">": "&gt;",
496
+ '"': "&quot;",
497
+ "'": "&#39;"
498
+ };
499
+ return text.replace(/[&<>"']/g, (char) => map[char] || char);
500
+ }
501
+ function generateId(text) {
502
+ return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").trim();
503
+ }
504
+ function sanitizeHtml(html) {
505
+ try {
506
+ if (html.includes("codepen.io/") || html.includes("youtube.com/embed/")) {
507
+ return html;
508
+ }
509
+ if (typeof DOMPurify?.sanitize === "function") {
510
+ const sanitized = DOMPurify.sanitize(html, {
511
+ ALLOWED_TAGS,
512
+ ALLOWED_ATTR,
513
+ ALLOW_DATA_ATTR: true,
514
+ ALLOW_UNKNOWN_PROTOCOLS: false,
515
+ SAFE_FOR_TEMPLATES: false,
516
+ WHOLE_DOCUMENT: false,
517
+ RETURN_DOM: false,
518
+ RETURN_DOM_FRAGMENT: false,
519
+ FORCE_BODY: false,
520
+ SANITIZE_DOM: false,
521
+ SANITIZE_NAMED_PROPS: false,
522
+ FORBID_ATTR: ["onload", "onerror", "onclick", "onmouseover", "onmouseout", "onfocus", "onblur"],
523
+ ADD_TAGS: ["iframe", "embed", "object", "param"],
524
+ ADD_ATTR: [
525
+ "allow",
526
+ "allowfullscreen",
527
+ "frameborder",
528
+ "scrolling",
529
+ "allowtransparency",
530
+ "sandbox",
531
+ "loading",
532
+ "style",
533
+ "title",
534
+ "name",
535
+ "seamless",
536
+ "srcdoc"
537
+ ],
538
+ ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|xxx):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
539
+ });
540
+ if (sanitized.length < html.length * 0.7) {
541
+ return basicSanitize(html);
542
+ }
543
+ return sanitized;
544
+ }
545
+ return basicSanitize(html);
546
+ } catch (error) {
547
+ console.error("Sanitization failed:", error);
548
+ return basicSanitize(html);
549
+ }
550
+ }
551
+ function basicSanitize(html) {
552
+ return html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/on\w+\s*=\s*"[^"]*"/gi, "").replace(/on\w+\s*=\s*'[^']*'/gi, "").replace(/javascript:/gi, "");
553
+ }
554
+ function extractDomain(url) {
555
+ try {
556
+ return new URL(url).hostname;
557
+ } catch {
558
+ return url;
559
+ }
560
+ }
561
+ function parseOptions(options) {
562
+ const parsed = {};
563
+ if (!options) return parsed;
564
+ options.split(",").forEach((option) => {
565
+ const [key, value] = option.split(":").map((s) => s.trim());
566
+ if (key && value) {
567
+ parsed[key] = value;
568
+ }
569
+ });
570
+ return parsed;
571
+ }
572
+
573
+ // src/renderer.ts
574
+ var MarkdownRenderer = class {
575
+ constructor(config) {
576
+ this.rules = /* @__PURE__ */ new Map();
577
+ this.warnings = [];
578
+ this.config = {
579
+ format: "tailwind",
580
+ sanitize: true,
581
+ allowUnsafeHtml: false,
582
+ debugMode: false,
583
+ ...config
584
+ };
585
+ this.setupDefaultRules();
586
+ }
587
+ addRule(rule) {
588
+ this.rules.set(rule.type, rule);
589
+ }
590
+ hasRule(type) {
591
+ return this.rules.has(type);
592
+ }
593
+ render(tokens) {
594
+ this.warnings = [];
595
+ const htmlParts = tokens.map((token) => this.renderToken(token));
596
+ const combinedHtml = htmlParts.join("");
597
+ if (this.config.sanitize && !this.config.allowUnsafeHtml) {
598
+ return sanitizeHtml(combinedHtml);
599
+ }
600
+ return combinedHtml;
601
+ }
602
+ getWarnings() {
603
+ return [...this.warnings];
604
+ }
605
+ getConfig() {
606
+ return { ...this.config };
607
+ }
608
+ updateConfig(config) {
609
+ this.config = { ...this.config, ...config };
610
+ }
611
+ setDebugMode(enabled) {
612
+ this.config.debugMode = enabled;
613
+ }
614
+ clearWarnings() {
615
+ this.warnings = [];
616
+ }
617
+ renderToken(token) {
618
+ const rule = this.rules.get(token.type);
619
+ if (rule) {
620
+ try {
621
+ return rule.render(token);
622
+ } catch (error) {
623
+ const errorMessage = error instanceof Error ? error.message : String(error);
624
+ this.warnings.push(`Render error for ${token.type}: ${errorMessage}`);
625
+ return this.createErrorBlock(`Render error for ${token.type}: ${errorMessage}`);
626
+ }
627
+ }
628
+ if (token.type === "text") {
629
+ return escapeHtml(token.content || token.raw || "");
630
+ }
631
+ if (this.config.debugMode) {
632
+ return this.createDebugBlock(token);
633
+ }
634
+ return escapeHtml(token.content || token.raw || "");
635
+ }
636
+ createErrorBlock(message) {
637
+ if (this.config.format === "html") {
638
+ return `<div style="background-color: #fee; border: 1px solid #fcc; color: #c66; padding: 8px; border-radius: 4px; margin: 8px 0; font-size: 14px;">
639
+ <strong>Render Error:</strong> ${escapeHtml(message)}
640
+ </div>`;
641
+ }
642
+ return `<div class="bg-red-100 border border-red-300 text-red-800 p-2 rounded text-sm mb-2">
643
+ <strong>Render Error:</strong> ${escapeHtml(message)}
644
+ </div>`;
645
+ }
646
+ createDebugBlock(token) {
647
+ if (this.config.format === "html") {
648
+ return `<div style="background-color: #fffbf0; border: 1px solid #fed; color: #b8860b; padding: 8px; border-radius: 4px; margin: 8px 0; font-size: 14px;">
649
+ <strong>Unknown token type:</strong> ${escapeHtml(token.type)}<br>
650
+ <strong>Content:</strong> ${escapeHtml(token.content || token.raw || "")}
651
+ </div>`;
652
+ }
653
+ return `<div class="bg-yellow-100 border border-yellow-300 text-yellow-800 p-2 rounded text-sm mb-2">
654
+ <strong>Unknown token type:</strong> ${escapeHtml(token.type)}<br>
655
+ <strong>Content:</strong> ${escapeHtml(token.content || token.raw || "")}
656
+ </div>`;
657
+ }
658
+ setupDefaultRules() {
659
+ this.addRule({
660
+ type: "heading",
661
+ render: (token) => {
662
+ const level = parseInt(token.attributes?.level || "1");
663
+ const text = token.content;
664
+ const id = generateId(text);
665
+ const escapedContent = escapeHtml(text);
666
+ if (this.config.format === "html") {
667
+ return `<h${level} id="${id}">${escapedContent}</h${level}>`;
668
+ }
669
+ let headingClasses = "group relative flex items-center gap-2";
670
+ switch (level) {
671
+ case 1:
672
+ headingClasses += " text-3xl font-bold mt-8 mb-4";
673
+ break;
674
+ case 2:
675
+ headingClasses += " text-2xl font-semibold mt-6 mb-3";
676
+ break;
677
+ case 3:
678
+ headingClasses += " text-xl font-medium mt-5 mb-3";
679
+ break;
680
+ case 4:
681
+ headingClasses += " text-lg font-medium mt-4 mb-2";
682
+ break;
683
+ case 5:
684
+ headingClasses += " text-base font-medium mt-3 mb-2";
685
+ break;
686
+ case 6:
687
+ headingClasses += " text-sm font-medium mt-3 mb-2";
688
+ break;
689
+ }
690
+ return `<h${level} id="${id}" class="${headingClasses}">
691
+ ${escapedContent}
692
+ <a href="#${id}" class="opacity-0 group-hover:opacity-100 text-muted-foreground transition-opacity">
693
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
694
+ <path d="M7.5 4H5.75A3.75 3.75 0 002 7.75v.5a3.75 3.75 0 003.75 3.75h1.5m-1.5-4h3m1.5-4h1.75A3.75 3.75 0 0114 7.75v.5a3.75 3.75 0 01-3.75 3.75H8.5"/>
695
+ </svg>
696
+ </a>
697
+ </h${level}>`;
698
+ }
699
+ });
700
+ this.addRule({
701
+ type: "bold",
702
+ render: (token) => {
703
+ const content = escapeHtml(token.content);
704
+ if (this.config.format === "html") {
705
+ return `<strong>${content}</strong>`;
706
+ }
707
+ return `<strong class="font-bold">${content}</strong>`;
708
+ }
709
+ });
710
+ this.addRule({
711
+ type: "italic",
712
+ render: (token) => {
713
+ const content = escapeHtml(token.content);
714
+ if (this.config.format === "html") {
715
+ return `<em>${content}</em>`;
716
+ }
717
+ return `<em class="italic">${content}</em>`;
718
+ }
719
+ });
720
+ this.addRule({
721
+ type: "code",
722
+ render: (token) => {
723
+ const content = escapeHtml(token.content);
724
+ if (this.config.format === "html") {
725
+ return `<code style="background-color: #f3f4f6; padding: 2px 6px; border-radius: 4px; font-family: monospace; font-size: 0.875rem;">${content}</code>`;
726
+ }
727
+ return `<code class="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">${content}</code>`;
728
+ }
729
+ });
730
+ this.addRule({
731
+ type: "codeblock",
732
+ render: (token) => {
733
+ const language = token.attributes?.language || "text";
734
+ const escapedCode = escapeHtml(token.content);
735
+ if (this.config.format === "html") {
736
+ return `<pre style="background-color: #f3f4f6; padding: 16px; border-radius: 6px; overflow-x: auto; margin: 16px 0;"><code class="language-${escapeHtml(language)}">${escapedCode}</code></pre>`;
737
+ }
738
+ return `<pre class="bg-muted p-4 rounded-md overflow-x-auto my-4"><code class="language-${escapeHtml(language)}">${escapedCode}</code></pre>`;
739
+ }
740
+ });
741
+ this.addRule({
742
+ type: "link",
743
+ render: (token) => {
744
+ const href = token.attributes?.href || "#";
745
+ const escapedHref = escapeHtml(href);
746
+ const escapedText = escapeHtml(token.content);
747
+ if (this.config.format === "html") {
748
+ return `<a href="${escapedHref}" target="_blank" rel="noopener noreferrer">${escapedText}</a>`;
749
+ }
750
+ return `<a href="${escapedHref}" class="text-primary hover:underline inline-flex items-center gap-1" target="_blank" rel="noopener noreferrer">
751
+ ${escapedText}
752
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-external-link">
753
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
754
+ <polyline points="15 3 21 3 21 9"></polyline>
755
+ <line x1="10" y1="14" x2="21" y2="3"></line>
756
+ </svg>
757
+ </a>`;
758
+ }
759
+ });
760
+ this.addRule({
761
+ type: "list-item",
762
+ render: (token) => `<li>${escapeHtml(token.content)}</li>`
763
+ });
764
+ this.addRule({
765
+ type: "blockquote",
766
+ render: (token) => {
767
+ const content = escapeHtml(token.content);
768
+ if (this.config.format === "html") {
769
+ return `<blockquote style="border-left: 2px solid #d1d5db; padding: 8px 0 8px 16px; margin: 16px 0; font-style: italic; color: #6b7280;">${content}</blockquote>`;
770
+ }
771
+ return `<blockquote class="pl-4 py-2 border-l-2 border-border italic text-muted-foreground my-4">${content}</blockquote>`;
772
+ }
773
+ });
774
+ this.addRule({
775
+ type: "text",
776
+ render: (token) => {
777
+ if (!token.content) return "";
778
+ return escapeHtml(token.content);
779
+ }
780
+ });
781
+ this.addRule({
782
+ type: "paragraph",
783
+ render: (token) => {
784
+ if (!token.content) return "";
785
+ const content = token.content.trim();
786
+ if (!content) return "";
787
+ const processedContent = content.includes("<br>") ? content : escapeHtml(content);
788
+ if (this.config.format === "html") {
789
+ return `<p style="line-height: 1.75; margin-bottom: 16px;">${processedContent}</p>`;
790
+ }
791
+ return `<p class="leading-7 mb-4">${processedContent}</p>`;
792
+ }
793
+ });
794
+ this.addRule({
795
+ type: "task-item",
796
+ render: (token) => {
797
+ const isChecked = token.attributes?.checked === "true";
798
+ const escapedContent = escapeHtml(token.content);
799
+ if (this.config.format === "html") {
800
+ return `<div style="display: flex; align-items: center; gap: 8px; margin: 8px 0;">
801
+ <input type="checkbox" ${isChecked ? "checked" : ""} disabled style="margin: 0;" />
802
+ <span${isChecked ? ' style="text-decoration: line-through; color: #6b7280;"' : ""}>${escapedContent}</span>
803
+ </div>`;
804
+ }
805
+ return `<div class="flex items-center gap-2 my-2 task-list-item">
806
+ <input type="checkbox" ${isChecked ? "checked" : ""} disabled
807
+ class="form-checkbox h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary" />
808
+ <span${isChecked ? ' class="line-through text-muted-foreground"' : ""}>${escapedContent}</span>
809
+ </div>`;
810
+ }
811
+ });
812
+ this.addRule({
813
+ type: "image",
814
+ render: (token) => {
815
+ const src = token.attributes?.src || "";
816
+ const alt = token.attributes?.alt || "";
817
+ const title = token.attributes?.title || "";
818
+ const titleAttr = title ? ` title="${escapeHtml(title)}"` : "";
819
+ if (this.config.format === "html") {
820
+ return `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}"${titleAttr} style="max-width: 100%; height: auto; border-radius: 8px; margin: 16px 0;" loading="lazy" />`;
821
+ }
822
+ return `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}"${titleAttr} class="max-w-full h-auto rounded-lg my-4" loading="lazy" />`;
823
+ }
824
+ });
825
+ this.addRule({
826
+ type: "hr",
827
+ render: () => {
828
+ if (this.config.format === "html") {
829
+ return '<hr style="margin: 24px 0; border: none; border-top: 1px solid #d1d5db;">';
830
+ }
831
+ return '<hr class="my-6 border-t border-border">';
832
+ }
833
+ });
834
+ this.addRule({
835
+ type: "line-break",
836
+ render: () => "<br>"
837
+ });
838
+ this.addRule({
839
+ type: "paragraph-break",
840
+ render: () => "</p><p>"
841
+ });
842
+ this.addRule({
843
+ type: "soft-break",
844
+ render: (token) => token.content || " "
845
+ });
846
+ }
847
+ };
848
+
849
+ // src/extensions/alert.ts
850
+ var AlertExtension = {
851
+ name: "alert",
852
+ parseRules: [
853
+ {
854
+ name: "alert",
855
+ pattern: /:::(\w+)(?:\s+(.*?))?\s*\n([\s\S]*?)\n:::/,
856
+ render: (match) => {
857
+ return {
858
+ type: "alert",
859
+ content: match[3]?.trim() || "",
860
+ raw: match[0] || "",
861
+ attributes: {
862
+ type: match[1] || "info",
863
+ title: match[2] || ""
864
+ }
865
+ };
866
+ }
867
+ }
868
+ ],
869
+ renderRules: [
870
+ {
871
+ type: "alert",
872
+ render: (token) => {
873
+ const type = token.attributes?.type || "info";
874
+ const title = token.attributes?.title || "";
875
+ const typeConfig = {
876
+ info: {
877
+ icon: "\u2139\uFE0F",
878
+ classes: "bg-blue-500/10 border-blue-500/30 text-blue-600 border-l-blue-500"
879
+ },
880
+ warning: {
881
+ icon: "\u26A0\uFE0F",
882
+ classes: "bg-amber-500/10 border-amber-500/30 text-amber-600 border-l-amber-500"
883
+ },
884
+ error: {
885
+ icon: "\u274C",
886
+ classes: "bg-red-500/10 border-red-500/30 text-red-600 border-l-red-500"
887
+ },
888
+ success: {
889
+ icon: "\u2705",
890
+ classes: "bg-green-500/10 border-green-500/30 text-green-600 border-l-green-500"
891
+ },
892
+ tip: {
893
+ icon: "\u{1F4A1}",
894
+ classes: "bg-purple-500/10 border-purple-500/30 text-purple-600 border-l-purple-500"
895
+ },
896
+ note: {
897
+ icon: "\u{1F4DD}",
898
+ classes: "bg-gray-500/10 border-gray-500/30 text-gray-600 border-l-gray-500"
899
+ }
900
+ };
901
+ const config = typeConfig[type] || typeConfig.info;
902
+ const baseClasses = "border-l-4 p-4 mb-4 rounded-md transition-colors duration-200";
903
+ const classes = `${baseClasses} ${config?.classes}`;
904
+ const titleHtml = title ? `<div class="font-medium mb-2 flex items-center gap-2">
905
+ <span class="text-lg" role="img" aria-label="${type}">${config?.icon}</span>
906
+ <span>${title}</span>
907
+ </div>` : `<div class="font-medium mb-2 flex items-center gap-2">
908
+ <span class="text-lg" role="img" aria-label="${type}">${config?.icon}</span>
909
+ <span>${type.charAt(0).toUpperCase() + type.slice(1)}</span>
910
+ </div>`;
911
+ return `<div class="${classes}" role="alert" aria-live="polite">
912
+ ${titleHtml}
913
+ <div class="leading-relaxed">${token.content}</div>
914
+ </div>`;
915
+ }
916
+ }
917
+ ]
918
+ };
919
+
920
+ // src/extensions/button.ts
921
+ var ButtonExtension = {
922
+ name: "button",
923
+ parseRules: [
924
+ {
925
+ name: "button",
926
+ pattern: /\[button:([^\]]+)\]\(([^)]+)\)(?:\{([^}]+)\})?/,
927
+ render: (match) => {
928
+ const text = match[1] || "";
929
+ const href = match[2] || "";
930
+ const optionsString = match[3] || "";
931
+ const options = optionsString.split(",").map((opt) => opt.trim()).filter(Boolean);
932
+ const styleOptions = ["default", "primary", "secondary", "success", "danger", "outline", "ghost"];
933
+ const style = options.find((opt) => styleOptions.includes(opt)) || "primary";
934
+ const sizeOptions = ["sm", "md", "lg"];
935
+ const size = options.find((opt) => sizeOptions.includes(opt)) || "md";
936
+ const disabled = options.includes("disabled");
937
+ const target = options.includes("self") ? "_self" : "_blank";
938
+ return {
939
+ type: "button",
940
+ content: text,
941
+ raw: match[0] || "",
942
+ attributes: {
943
+ href,
944
+ style,
945
+ size,
946
+ disabled: disabled.toString(),
947
+ target
948
+ }
949
+ };
950
+ }
951
+ }
952
+ ],
953
+ renderRules: [
954
+ {
955
+ type: "button",
956
+ render: (token) => {
957
+ const href = token.attributes?.href || "#";
958
+ const style = token.attributes?.style || "primary";
959
+ const size = token.attributes?.size || "md";
960
+ const disabled = token.attributes?.disabled === "true";
961
+ const target = token.attributes?.target || "_blank";
962
+ const text = token.content;
963
+ const classes = buildButtonClasses(style, size);
964
+ const targetAttr = target === "_blank" ? ' target="_blank" rel="noopener noreferrer"' : target === "_self" ? ' target="_self"' : "";
965
+ const disabledAttr = disabled ? ' aria-disabled="true" tabindex="-1"' : "";
966
+ const externalIcon = target === "_blank" && !disabled ? '<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>' : "";
967
+ return `<a href="${href}" class="${classes}"${targetAttr}${disabledAttr}>
968
+ ${text}${externalIcon}
969
+ </a>`;
970
+ }
971
+ }
972
+ ]
973
+ };
974
+ function buildButtonClasses(style, size) {
975
+ const base = [
976
+ "inline-flex",
977
+ "items-center",
978
+ "justify-center",
979
+ "font-medium",
980
+ "rounded-lg",
981
+ "transition-colors",
982
+ "focus:outline-none",
983
+ "focus:ring-2",
984
+ "focus:ring-offset-2",
985
+ "disabled:opacity-50",
986
+ "disabled:cursor-not-allowed"
987
+ ];
988
+ const sizes = {
989
+ sm: ["px-3", "py-1.5", "text-sm"],
990
+ md: ["px-4", "py-2", "text-base"],
991
+ lg: ["px-6", "py-3", "text-lg"]
992
+ };
993
+ const styles = {
994
+ default: ["bg-slate-600", "text-white", "hover:bg-slate-700", "focus:ring-slate-500"],
995
+ primary: ["bg-blue-600", "text-white", "hover:bg-blue-700", "focus:ring-blue-500"],
996
+ secondary: ["bg-gray-600", "text-white", "hover:bg-gray-700", "focus:ring-gray-500"],
997
+ success: ["bg-green-600", "text-white", "hover:bg-green-700", "focus:ring-green-500"],
998
+ danger: ["bg-red-600", "text-white", "hover:bg-red-700", "focus:ring-red-500"],
999
+ outline: ["border", "border-blue-600", "text-blue-600", "hover:bg-blue-50", "focus:ring-blue-500"],
1000
+ ghost: ["text-gray-700", "hover:bg-gray-100", "focus:ring-gray-500"]
1001
+ };
1002
+ const allClasses = [
1003
+ ...base,
1004
+ ...sizes[size] ?? sizes.md ?? [],
1005
+ ...styles[style] ?? styles.primary ?? []
1006
+ ];
1007
+ return allClasses.join(" ");
1008
+ }
1009
+
1010
+ // src/extensions/embed.ts
1011
+ var EmbedExtension = {
1012
+ name: "embed",
1013
+ parseRules: [
1014
+ {
1015
+ name: "embed",
1016
+ pattern: /\[embed:(\w+)\]\(([^)]+)\)(?:\{([^}]+)\})?/,
1017
+ render: (match) => {
1018
+ return {
1019
+ type: "embed",
1020
+ content: match[2] || "",
1021
+ // URL
1022
+ raw: match[0] || "",
1023
+ attributes: {
1024
+ provider: match[1] || "generic",
1025
+ url: match[2] || "",
1026
+ options: match[3] || ""
1027
+ }
1028
+ };
1029
+ }
1030
+ }
1031
+ ],
1032
+ renderRules: [
1033
+ {
1034
+ type: "embed",
1035
+ render: (token) => {
1036
+ const provider = token.attributes?.provider || "generic";
1037
+ const url = token.attributes?.url || "";
1038
+ const options = token.attributes?.options || "";
1039
+ return renderEmbed(provider, url, options);
1040
+ }
1041
+ }
1042
+ ]
1043
+ };
1044
+ function renderEmbed(provider, url, options) {
1045
+ const baseClasses = "rounded-lg border bg-card text-card-foreground shadow-sm mb-6 overflow-hidden";
1046
+ const parsedOptions = parseOptions(options);
1047
+ switch (provider.toLowerCase()) {
1048
+ case "youtube":
1049
+ return renderYouTubeEmbed(url, parsedOptions, baseClasses);
1050
+ case "codepen":
1051
+ return renderCodePenEmbed(url, parsedOptions, baseClasses);
1052
+ case "figma":
1053
+ return renderFigmaEmbed(url, parsedOptions, baseClasses);
1054
+ case "twitter":
1055
+ case "tweet":
1056
+ return renderTwitterEmbed(url, parsedOptions, baseClasses);
1057
+ case "github":
1058
+ return renderGitHubEmbed(url, parsedOptions, baseClasses);
1059
+ case "vimeo":
1060
+ return renderVimeoEmbed(url, parsedOptions, baseClasses);
1061
+ case "spotify":
1062
+ return renderSpotifyEmbed(url, parsedOptions, baseClasses);
1063
+ case "codesandbox":
1064
+ return renderCodeSandboxEmbed(url, parsedOptions, baseClasses);
1065
+ default:
1066
+ return renderGenericEmbed(url, parsedOptions, baseClasses);
1067
+ }
1068
+ }
1069
+ function renderYouTubeEmbed(url, options, classes) {
1070
+ const videoId = extractYouTubeId(url);
1071
+ if (!videoId) {
1072
+ return createErrorEmbed("Invalid YouTube URL", url, classes);
1073
+ }
1074
+ const params = new URLSearchParams();
1075
+ if (options.autoplay === "1") params.set("autoplay", "1");
1076
+ if (options.mute === "1") params.set("mute", "1");
1077
+ if (options.loop === "1") {
1078
+ params.set("loop", "1");
1079
+ params.set("playlist", videoId);
1080
+ }
1081
+ if (options.controls === "0") params.set("controls", "0");
1082
+ if (options.start) params.set("start", options.start);
1083
+ params.set("rel", "0");
1084
+ params.set("modestbranding", "1");
1085
+ const embedUrl = `https://www.youtube.com/embed/${videoId}?${params.toString()}`;
1086
+ return `
1087
+ <div class="${classes}">
1088
+ <div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
1089
+ <iframe
1090
+ style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0;"
1091
+ src="${embedUrl}"
1092
+ title="YouTube video player"
1093
+ frameborder="0"
1094
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
1095
+ allowfullscreen>
1096
+ </iframe>
1097
+ </div>
1098
+ </div>`;
1099
+ }
1100
+ function renderCodePenEmbed(url, options, classes) {
1101
+ const match = url.match(/codepen\.io\/([^\/]+)\/(?:pen|embed)\/([^\/\?#]+)/);
1102
+ if (!match) {
1103
+ return createErrorEmbed("Invalid CodePen URL", url, classes);
1104
+ }
1105
+ const [, user, penId] = match;
1106
+ const height = options.height || "400";
1107
+ const theme = options.theme === "light" ? "light" : "dark";
1108
+ const defaultTab = options.tab || "result";
1109
+ const embedParams = new URLSearchParams({
1110
+ "default-tab": defaultTab,
1111
+ "theme-id": theme,
1112
+ "editable": "true"
1113
+ });
1114
+ const embedUrl = `https://codepen.io/${user}/embed/${penId}?${embedParams.toString()}`;
1115
+ return `
1116
+ <div class="${classes}">
1117
+ <iframe
1118
+ height="${height}"
1119
+ style="width: 100%; border: 0;"
1120
+ scrolling="no"
1121
+ title="CodePen Embed - ${penId}"
1122
+ src="${embedUrl}"
1123
+ frameborder="0"
1124
+ loading="lazy"
1125
+ allowtransparency="true"
1126
+ allowfullscreen="true">
1127
+ </iframe>
1128
+ </div>`;
1129
+ }
1130
+ function renderVimeoEmbed(url, options, classes) {
1131
+ const videoId = extractVimeoId(url);
1132
+ if (!videoId) {
1133
+ return createErrorEmbed("Invalid Vimeo URL", url, classes);
1134
+ }
1135
+ const params = new URLSearchParams();
1136
+ if (options.autoplay === "1") params.set("autoplay", "1");
1137
+ if (options.mute === "1") params.set("muted", "1");
1138
+ if (options.loop === "1") params.set("loop", "1");
1139
+ const embedUrl = `https://player.vimeo.com/video/${videoId}?${params.toString()}`;
1140
+ return `
1141
+ <div class="${classes}">
1142
+ <div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
1143
+ <iframe
1144
+ style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0;"
1145
+ src="${embedUrl}"
1146
+ title="Vimeo video player"
1147
+ frameborder="0"
1148
+ allow="autoplay; fullscreen; picture-in-picture"
1149
+ allowfullscreen>
1150
+ </iframe>
1151
+ </div>
1152
+ </div>`;
1153
+ }
1154
+ function renderSpotifyEmbed(url, options, classes) {
1155
+ const embedUrl = url.replace("open.spotify.com", "open.spotify.com/embed");
1156
+ const height = options.height || "380";
1157
+ return `
1158
+ <div class="${classes}">
1159
+ <iframe
1160
+ style="border-radius: 12px;"
1161
+ src="${embedUrl}"
1162
+ width="100%"
1163
+ height="${height}"
1164
+ frameborder="0"
1165
+ allowfullscreen=""
1166
+ allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
1167
+ loading="lazy">
1168
+ </iframe>
1169
+ </div>`;
1170
+ }
1171
+ function renderCodeSandboxEmbed(url, options, classes) {
1172
+ let embedUrl = url;
1173
+ if (url.includes("/s/")) {
1174
+ embedUrl = url.replace("/s/", "/embed/");
1175
+ }
1176
+ const height = options.height || "500";
1177
+ const view = options.view || "preview";
1178
+ if (!embedUrl.includes("?")) {
1179
+ embedUrl += `?view=${view}`;
1180
+ }
1181
+ return `
1182
+ <div class="${classes}">
1183
+ <iframe
1184
+ src="${embedUrl}"
1185
+ style="width: 100%; height: ${height}px; border: 0; border-radius: 4px; overflow: hidden;"
1186
+ title="CodeSandbox Embed"
1187
+ allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
1188
+ sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts">
1189
+ </iframe>
1190
+ </div>`;
1191
+ }
1192
+ function renderFigmaEmbed(url, options, classes) {
1193
+ const embedUrl = `https://www.figma.com/embed?embed_host=share&url=${encodeURIComponent(url)}`;
1194
+ const height = options.height || "450";
1195
+ return `
1196
+ <div class="${classes}">
1197
+ <iframe
1198
+ style="border: none;"
1199
+ width="100%"
1200
+ height="${height}"
1201
+ src="${embedUrl}"
1202
+ allowfullscreen>
1203
+ </iframe>
1204
+ </div>`;
1205
+ }
1206
+ function renderTwitterEmbed(url, _options, classes) {
1207
+ return `
1208
+ <div class="${classes}">
1209
+ <div class="p-4">
1210
+ <div class="flex items-center gap-3 mb-3">
1211
+ <svg class="w-6 h-6 fill-current text-blue-500" viewBox="0 0 24 24">
1212
+ <path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
1213
+ </svg>
1214
+ <div>
1215
+ <div class="font-semibold text-foreground">Twitter Post</div>
1216
+ <div class="text-sm text-muted-foreground">External Link</div>
1217
+ </div>
1218
+ </div>
1219
+ <a href="${url}" target="_blank"
1220
+ class="inline-flex items-center gap-2 text-primary hover:text-primary/80 font-medium transition-colors">
1221
+ View on Twitter
1222
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1223
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
1224
+ </svg>
1225
+ </a>
1226
+ </div>
1227
+ </div>`;
1228
+ }
1229
+ function renderGitHubEmbed(url, _options, classes) {
1230
+ const parts = url.replace("https://github.com/", "").split("/");
1231
+ const owner = parts[0];
1232
+ const repo = parts[1];
1233
+ if (!owner || !repo) {
1234
+ return createErrorEmbed("Invalid GitHub URL", url, classes);
1235
+ }
1236
+ return `
1237
+ <div class="${classes}">
1238
+ <div class="p-4">
1239
+ <div class="flex items-center gap-3 mb-3">
1240
+ <svg class="w-6 h-6 fill-current" viewBox="0 0 24 24">
1241
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
1242
+ </svg>
1243
+ <div>
1244
+ <div class="font-semibold text-foreground text-lg">${owner}/${repo}</div>
1245
+ <div class="text-sm text-muted-foreground">GitHub Repository</div>
1246
+ </div>
1247
+ </div>
1248
+ <a href="${url}" target="_blank"
1249
+ class="inline-flex items-center gap-2 text-primary hover:text-primary/80 font-medium transition-colors">
1250
+ View on GitHub
1251
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1252
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
1253
+ </svg>
1254
+ </a>
1255
+ </div>
1256
+ </div>`;
1257
+ }
1258
+ function renderGenericEmbed(url, _options, classes) {
1259
+ const domain = extractDomain(url);
1260
+ return `
1261
+ <div class="${classes}">
1262
+ <div class="p-4">
1263
+ <div class="flex items-center gap-3 mb-3">
1264
+ <div class="w-10 h-10 rounded-lg bg-muted flex items-center justify-center">
1265
+ <svg class="w-5 h-5 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1266
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
1267
+ </svg>
1268
+ </div>
1269
+ <div>
1270
+ <div class="font-semibold text-foreground">External Link</div>
1271
+ <div class="text-sm text-muted-foreground">${domain}</div>
1272
+ </div>
1273
+ </div>
1274
+ <a href="${url}" target="_blank"
1275
+ class="inline-flex items-center gap-2 text-primary hover:text-primary/80 font-medium transition-colors break-all">
1276
+ ${url}
1277
+ <svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1278
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
1279
+ </svg>
1280
+ </a>
1281
+ </div>
1282
+ </div>`;
1283
+ }
1284
+ function createErrorEmbed(error, url, classes) {
1285
+ return `
1286
+ <div class="${classes}">
1287
+ <div class="p-4 text-destructive">
1288
+ <div class="font-medium flex items-center gap-2">
1289
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1290
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
1291
+ </svg>
1292
+ ${error}
1293
+ </div>
1294
+ <div class="text-sm text-muted-foreground mt-1 break-all">${url}</div>
1295
+ </div>
1296
+ </div>`;
1297
+ }
1298
+ function extractYouTubeId(url) {
1299
+ const patterns = [
1300
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/)([^&\n?#]+)/,
1301
+ /youtube\.com\/watch\?.*v=([^&\n?#]+)/
1302
+ ];
1303
+ for (const pattern of patterns) {
1304
+ const match = url.match(pattern);
1305
+ if (match) return match[1] || null;
1306
+ }
1307
+ return null;
1308
+ }
1309
+ function extractVimeoId(url) {
1310
+ const match = url.match(/vimeo\.com\/(?:.*\/)?(\d+)/);
1311
+ return match?.[1] ?? null;
1312
+ }
1313
+
1314
+ // src/engine.ts
1315
+ var ChangerawrMarkdown = class {
1316
+ constructor(config) {
1317
+ this.extensions = /* @__PURE__ */ new Map();
1318
+ this.parser = new MarkdownParser(config?.parser);
1319
+ this.renderer = new MarkdownRenderer(config?.renderer);
1320
+ this.registerBuiltInExtensions();
1321
+ if (config?.extensions) {
1322
+ config.extensions.forEach((extension) => {
1323
+ this.registerExtension(extension);
1324
+ });
1325
+ }
1326
+ this.parser.setupDefaultRulesIfEmpty();
1327
+ }
1328
+ /**
1329
+ * Register a custom extension
1330
+ */
1331
+ registerExtension(extension) {
1332
+ try {
1333
+ this.extensions.set(extension.name, extension);
1334
+ extension.parseRules.forEach((rule) => {
1335
+ this.parser.addRule(rule);
1336
+ });
1337
+ extension.renderRules.forEach((rule) => {
1338
+ this.renderer.addRule(rule);
1339
+ });
1340
+ return {
1341
+ success: true,
1342
+ extensionName: extension.name
1343
+ };
1344
+ } catch (error) {
1345
+ const errorMessage = error instanceof Error ? error.message : String(error);
1346
+ return {
1347
+ success: false,
1348
+ extensionName: extension.name,
1349
+ error: errorMessage
1350
+ };
1351
+ }
1352
+ }
1353
+ /**
1354
+ * Unregister an extension
1355
+ */
1356
+ unregisterExtension(name) {
1357
+ const extension = this.extensions.get(name);
1358
+ if (!extension) {
1359
+ return false;
1360
+ }
1361
+ try {
1362
+ this.extensions.delete(name);
1363
+ this.rebuildParserAndRenderer();
1364
+ return true;
1365
+ } catch {
1366
+ return false;
1367
+ }
1368
+ }
1369
+ /**
1370
+ * Parse markdown content into tokens
1371
+ */
1372
+ parse(markdown2) {
1373
+ return this.parser.parse(markdown2);
1374
+ }
1375
+ /**
1376
+ * Render tokens to HTML
1377
+ */
1378
+ render(tokens) {
1379
+ return this.renderer.render(tokens);
1380
+ }
1381
+ /**
1382
+ * Parse and render markdown to HTML in one step
1383
+ */
1384
+ toHtml(markdown2) {
1385
+ const tokens = this.parse(markdown2);
1386
+ return this.render(tokens);
1387
+ }
1388
+ /**
1389
+ * Get list of registered extensions
1390
+ */
1391
+ getExtensions() {
1392
+ return Array.from(this.extensions.keys());
1393
+ }
1394
+ /**
1395
+ * Check if extension is registered
1396
+ */
1397
+ hasExtension(name) {
1398
+ return this.extensions.has(name);
1399
+ }
1400
+ /**
1401
+ * Get parser warnings
1402
+ */
1403
+ getWarnings() {
1404
+ return [...this.parser.getWarnings(), ...this.renderer.getWarnings()];
1405
+ }
1406
+ /**
1407
+ * Get debug information from last render
1408
+ */
1409
+ getDebugInfo() {
1410
+ return {
1411
+ warnings: this.getWarnings(),
1412
+ parseTime: 0,
1413
+ renderTime: 0,
1414
+ tokenCount: 0,
1415
+ iterationCount: 0
1416
+ };
1417
+ }
1418
+ /**
1419
+ * Get performance metrics for the last operation
1420
+ */
1421
+ getPerformanceMetrics() {
1422
+ return {
1423
+ parseTime: 0,
1424
+ renderTime: 0,
1425
+ totalTime: 0,
1426
+ tokenCount: 0
1427
+ };
1428
+ }
1429
+ /**
1430
+ * Register built-in extensions
1431
+ */
1432
+ registerBuiltInExtensions() {
1433
+ this.registerExtension(AlertExtension);
1434
+ this.registerExtension(ButtonExtension);
1435
+ this.registerExtension(EmbedExtension);
1436
+ }
1437
+ /**
1438
+ * Rebuild parser and renderer with current extensions
1439
+ */
1440
+ rebuildParserAndRenderer() {
1441
+ const parserConfig = this.parser.getConfig();
1442
+ const rendererConfig = this.renderer.getConfig();
1443
+ this.parser = new MarkdownParser(parserConfig);
1444
+ this.renderer = new MarkdownRenderer(rendererConfig);
1445
+ const extensionsToRegister = Array.from(this.extensions.values());
1446
+ this.extensions.clear();
1447
+ extensionsToRegister.forEach((extension) => {
1448
+ this.registerExtension(extension);
1449
+ });
1450
+ this.parser.setupDefaultRulesIfEmpty();
1451
+ }
1452
+ };
1453
+ var markdown = new ChangerawrMarkdown();
1454
+
1455
+ // src/react/hooks.ts
1456
+ function useMarkdown(initialContent = "", options = {}) {
1457
+ const [content, setContent] = useState(initialContent);
1458
+ const [html, setHtml] = useState("");
1459
+ const [tokens, setTokens] = useState([]);
1460
+ const [isLoading, setIsLoading] = useState(false);
1461
+ const [error, setError] = useState(null);
1462
+ const [debug, setDebug] = useState(null);
1463
+ const engineRef = useRef(null);
1464
+ const engine = useMemo(() => {
1465
+ const format = options.format ?? "tailwind";
1466
+ const rendererConfig = {
1467
+ format,
1468
+ ...options.config?.renderer && {
1469
+ sanitize: options.config.renderer.sanitize,
1470
+ allowUnsafeHtml: options.config.renderer.allowUnsafeHtml,
1471
+ customClasses: options.config.renderer.customClasses,
1472
+ debugMode: options.debug ?? options.config.renderer.debugMode ?? false
1473
+ }
1474
+ };
1475
+ const engineConfig = {
1476
+ ...options.config && {
1477
+ parser: options.config.parser,
1478
+ extensions: options.config.extensions
1479
+ },
1480
+ renderer: rendererConfig
1481
+ };
1482
+ const newEngine = new ChangerawrMarkdown(engineConfig);
1483
+ if (options.extensions) {
1484
+ options.extensions.forEach((extension) => {
1485
+ newEngine.registerExtension(extension);
1486
+ });
1487
+ }
1488
+ engineRef.current = newEngine;
1489
+ return newEngine;
1490
+ }, [options.config, options.format, options.debug, options.extensions]);
1491
+ const processMarkdown = useCallback((markdownContent) => {
1492
+ if (!markdownContent.trim()) {
1493
+ setHtml("");
1494
+ setTokens([]);
1495
+ setError(null);
1496
+ setDebug(null);
1497
+ setIsLoading(false);
1498
+ return;
1499
+ }
1500
+ setIsLoading(true);
1501
+ setError(null);
1502
+ try {
1503
+ const parsedTokens = engine.parse(markdownContent);
1504
+ const renderedHtml = engine.render(parsedTokens);
1505
+ setHtml(renderedHtml);
1506
+ setTokens(parsedTokens);
1507
+ if (options.debug) {
1508
+ const coreDebug = engine.getDebugInfo();
1509
+ const performanceMetrics = engine.getPerformanceMetrics();
1510
+ setDebug({
1511
+ core: coreDebug,
1512
+ performance: performanceMetrics,
1513
+ renderedAt: /* @__PURE__ */ new Date(),
1514
+ contentLength: markdownContent.length,
1515
+ htmlLength: renderedHtml.length,
1516
+ extensionsUsed: engine.getExtensions()
1517
+ });
1518
+ }
1519
+ } catch (err) {
1520
+ const errorObj = err instanceof Error ? err : new Error(String(err));
1521
+ setError(errorObj);
1522
+ } finally {
1523
+ setIsLoading(false);
1524
+ }
1525
+ }, [engine, options.debug]);
1526
+ useEffect(() => {
1527
+ processMarkdown(content);
1528
+ }, [content, processMarkdown]);
1529
+ useEffect(() => {
1530
+ setContent(initialContent);
1531
+ }, [initialContent]);
1532
+ const render = useCallback((newContent) => {
1533
+ setContent(newContent);
1534
+ }, []);
1535
+ const clear = useCallback(() => {
1536
+ setContent("");
1537
+ setHtml("");
1538
+ setTokens([]);
1539
+ setError(null);
1540
+ setDebug(null);
1541
+ }, []);
1542
+ return {
1543
+ html,
1544
+ tokens,
1545
+ isLoading,
1546
+ error,
1547
+ debug,
1548
+ render,
1549
+ clear
1550
+ };
1551
+ }
1552
+ function useMarkdownEngine(options = {}) {
1553
+ const engineRef = useRef(null);
1554
+ const engine = useMemo(() => {
1555
+ const engineConfig = {
1556
+ ...options.config,
1557
+ renderer: {
1558
+ format: "tailwind",
1559
+ // Required field
1560
+ ...options.config?.renderer
1561
+ }
1562
+ };
1563
+ if (options.autoRegisterExtensions === false) {
1564
+ engineConfig.extensions = [];
1565
+ }
1566
+ const newEngine = new ChangerawrMarkdown(engineConfig);
1567
+ engineRef.current = newEngine;
1568
+ return newEngine;
1569
+ }, [options.config, options.autoRegisterExtensions]);
1570
+ const toHtml = useCallback((content) => {
1571
+ return engine.toHtml(content);
1572
+ }, [engine]);
1573
+ const parse = useCallback((content) => {
1574
+ return engine.parse(content);
1575
+ }, [engine]);
1576
+ const render = useCallback((tokens) => {
1577
+ return engine.render(tokens);
1578
+ }, [engine]);
1579
+ const getExtensions = useCallback(() => {
1580
+ return engine.getExtensions();
1581
+ }, [engine]);
1582
+ const hasExtension = useCallback((name) => {
1583
+ return engine.hasExtension(name);
1584
+ }, [engine]);
1585
+ const registerExtension = useCallback((extension) => {
1586
+ return engine.registerExtension(extension);
1587
+ }, [engine]);
1588
+ const unregisterExtension = useCallback((name) => {
1589
+ return engine.unregisterExtension(name);
1590
+ }, [engine]);
1591
+ const getWarnings = useCallback(() => {
1592
+ return engine.getWarnings();
1593
+ }, [engine]);
1594
+ const getDebugInfo = useCallback(() => {
1595
+ return engine.getDebugInfo();
1596
+ }, [engine]);
1597
+ const getPerformanceMetrics = useCallback(() => {
1598
+ return engine.getPerformanceMetrics();
1599
+ }, [engine]);
1600
+ return {
1601
+ engine,
1602
+ toHtml,
1603
+ parse,
1604
+ render,
1605
+ getExtensions,
1606
+ hasExtension,
1607
+ registerExtension,
1608
+ unregisterExtension,
1609
+ getWarnings,
1610
+ getDebugInfo,
1611
+ getPerformanceMetrics
1612
+ };
1613
+ }
1614
+ function useMarkdownDebug(content) {
1615
+ const { html, tokens, debug } = useMarkdown(content, { debug: true });
1616
+ return {
1617
+ html,
1618
+ tokens,
1619
+ debug,
1620
+ stats: {
1621
+ tokenCount: tokens.length,
1622
+ htmlLength: html.length,
1623
+ contentLength: content.length
1624
+ }
1625
+ };
1626
+ }
1627
+
1628
+ // src/react/MarkdownRenderer.tsx
1629
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1630
+ function MarkdownRenderer2({
1631
+ content,
1632
+ className,
1633
+ config,
1634
+ format = "tailwind",
1635
+ as: Component = "div",
1636
+ wrapperProps = {},
1637
+ debug = false,
1638
+ errorFallback,
1639
+ loading,
1640
+ onRender,
1641
+ onError,
1642
+ extensions,
1643
+ sanitize = true,
1644
+ allowUnsafeHtml = false,
1645
+ ...restProps
1646
+ }) {
1647
+ const markdownOptions = useMemo2(() => {
1648
+ const options = {
1649
+ format,
1650
+ debug,
1651
+ debounceMs: 0,
1652
+ // No debounce for tests
1653
+ memoize: true
1654
+ };
1655
+ if (config) {
1656
+ options.config = {
1657
+ ...config,
1658
+ renderer: {
1659
+ format,
1660
+ // Required field
1661
+ sanitize,
1662
+ allowUnsafeHtml,
1663
+ debugMode: debug,
1664
+ ...config.renderer
1665
+ }
1666
+ };
1667
+ } else {
1668
+ options.config = {
1669
+ renderer: {
1670
+ format,
1671
+ sanitize,
1672
+ allowUnsafeHtml,
1673
+ debugMode: debug
1674
+ }
1675
+ };
1676
+ }
1677
+ if (extensions) {
1678
+ options.extensions = extensions;
1679
+ }
1680
+ return options;
1681
+ }, [config, format, debug, extensions, sanitize, allowUnsafeHtml]);
1682
+ const { html, tokens, isLoading, error } = useMarkdown(content, markdownOptions);
1683
+ useEffect2(() => {
1684
+ if (html && onRender) {
1685
+ onRender(html, tokens);
1686
+ }
1687
+ }, [html, tokens, onRender]);
1688
+ useEffect2(() => {
1689
+ if (error && onError) {
1690
+ onError(error);
1691
+ }
1692
+ }, [error, onError]);
1693
+ if (isLoading && loading) {
1694
+ return /* @__PURE__ */ jsx(Fragment, { children: loading });
1695
+ }
1696
+ if (error) {
1697
+ if (errorFallback) {
1698
+ return /* @__PURE__ */ jsx(Fragment, { children: errorFallback(error) });
1699
+ }
1700
+ return /* @__PURE__ */ jsxs("div", { className: "changerawr-error bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded", children: [
1701
+ /* @__PURE__ */ jsx("div", { className: "font-medium", children: "Markdown Render Error" }),
1702
+ /* @__PURE__ */ jsx("div", { className: "text-sm mt-1", children: error.message })
1703
+ ] });
1704
+ }
1705
+ const finalWrapperProps = {
1706
+ ...wrapperProps,
1707
+ ...restProps,
1708
+ className: className ? `${className} changerawr-markdown` : "changerawr-markdown",
1709
+ dangerouslySetInnerHTML: { __html: html }
1710
+ };
1711
+ return React.createElement(Component, finalWrapperProps);
1712
+ }
1713
+ MarkdownRenderer2.displayName = "MarkdownRenderer";
1714
+ var MarkdownErrorBoundary = class extends React.Component {
1715
+ constructor(props) {
1716
+ super(props);
1717
+ this.state = { hasError: false, error: null };
1718
+ }
1719
+ static getDerivedStateFromError(error) {
1720
+ return { hasError: true, error };
1721
+ }
1722
+ componentDidCatch(error, errorInfo) {
1723
+ console.error("MarkdownRenderer Error:", error, errorInfo);
1724
+ }
1725
+ render() {
1726
+ if (this.state.hasError && this.state.error) {
1727
+ if (this.props.fallback) {
1728
+ return this.props.fallback(this.state.error);
1729
+ }
1730
+ return /* @__PURE__ */ jsxs("div", { className: "changerawr-error-boundary bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded", children: [
1731
+ /* @__PURE__ */ jsx("div", { className: "font-medium", children: "Markdown Component Error" }),
1732
+ /* @__PURE__ */ jsx("div", { className: "text-sm mt-1", children: this.state.error.message }),
1733
+ /* @__PURE__ */ jsx(
1734
+ "button",
1735
+ {
1736
+ onClick: () => this.setState({ hasError: false, error: null }),
1737
+ className: "text-xs mt-2 px-2 py-1 bg-red-200 hover:bg-red-300 rounded",
1738
+ children: "Try Again"
1739
+ }
1740
+ )
1741
+ ] });
1742
+ }
1743
+ return this.props.children;
1744
+ }
1745
+ };
1746
+ export {
1747
+ MarkdownRenderer2 as MarkdownRenderer,
1748
+ useMarkdown,
1749
+ useMarkdownDebug,
1750
+ useMarkdownEngine
1751
+ };
1752
+ //# sourceMappingURL=index.mjs.map