@buenojs/bueno 0.8.3 → 0.8.5

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 (218) hide show
  1. package/README.md +136 -16
  2. package/dist/cli/{index.js → bin.js} +3036 -1421
  3. package/dist/container/index.js +250 -0
  4. package/dist/context/index.js +219 -0
  5. package/dist/database/index.js +493 -0
  6. package/dist/frontend/index.js +7697 -0
  7. package/dist/health/index.js +364 -0
  8. package/dist/i18n/index.js +345 -0
  9. package/dist/index.js +11043 -6482
  10. package/dist/jobs/index.js +819 -0
  11. package/dist/lock/index.js +367 -0
  12. package/dist/logger/index.js +281 -0
  13. package/dist/metrics/index.js +289 -0
  14. package/dist/middleware/index.js +77 -0
  15. package/dist/migrations/index.js +571 -0
  16. package/dist/modules/index.js +3346 -0
  17. package/dist/notification/index.js +484 -0
  18. package/dist/observability/index.js +331 -0
  19. package/dist/openapi/index.js +776 -0
  20. package/dist/orm/index.js +1356 -0
  21. package/dist/router/index.js +886 -0
  22. package/dist/rpc/index.js +691 -0
  23. package/dist/schema/index.js +400 -0
  24. package/dist/telemetry/index.js +595 -0
  25. package/dist/template/index.js +640 -0
  26. package/dist/templates/index.js +640 -0
  27. package/dist/testing/index.js +1111 -0
  28. package/dist/types/index.js +60 -0
  29. package/package.json +121 -27
  30. package/src/cache/index.ts +2 -1
  31. package/src/cli/bin.ts +2 -2
  32. package/src/cli/commands/build.ts +183 -165
  33. package/src/cli/commands/dev.ts +96 -89
  34. package/src/cli/commands/generate.ts +142 -111
  35. package/src/cli/commands/help.ts +20 -16
  36. package/src/cli/commands/index.ts +3 -6
  37. package/src/cli/commands/migration.ts +124 -105
  38. package/src/cli/commands/new.ts +392 -438
  39. package/src/cli/commands/start.ts +81 -79
  40. package/src/cli/core/args.ts +68 -50
  41. package/src/cli/core/console.ts +89 -95
  42. package/src/cli/core/index.ts +4 -4
  43. package/src/cli/core/prompt.ts +65 -62
  44. package/src/cli/core/spinner.ts +23 -20
  45. package/src/cli/index.ts +46 -38
  46. package/src/cli/templates/database/index.ts +61 -0
  47. package/src/cli/templates/database/mysql.ts +14 -0
  48. package/src/cli/templates/database/none.ts +16 -0
  49. package/src/cli/templates/database/postgresql.ts +14 -0
  50. package/src/cli/templates/database/sqlite.ts +14 -0
  51. package/src/cli/templates/deploy.ts +29 -26
  52. package/src/cli/templates/docker.ts +41 -30
  53. package/src/cli/templates/frontend/index.ts +63 -0
  54. package/src/cli/templates/frontend/none.ts +17 -0
  55. package/src/cli/templates/frontend/react.ts +140 -0
  56. package/src/cli/templates/frontend/solid.ts +134 -0
  57. package/src/cli/templates/frontend/svelte.ts +131 -0
  58. package/src/cli/templates/frontend/vue.ts +130 -0
  59. package/src/cli/templates/generators/index.ts +339 -0
  60. package/src/cli/templates/generators/types.ts +56 -0
  61. package/src/cli/templates/index.ts +35 -2
  62. package/src/cli/templates/project/api.ts +81 -0
  63. package/src/cli/templates/project/default.ts +140 -0
  64. package/src/cli/templates/project/fullstack.ts +111 -0
  65. package/src/cli/templates/project/index.ts +95 -0
  66. package/src/cli/templates/project/minimal.ts +45 -0
  67. package/src/cli/templates/project/types.ts +94 -0
  68. package/src/cli/templates/project/website.ts +263 -0
  69. package/src/cli/utils/fs.ts +55 -41
  70. package/src/cli/utils/index.ts +3 -2
  71. package/src/cli/utils/strings.ts +47 -33
  72. package/src/cli/utils/version.ts +47 -0
  73. package/src/config/env-validation.ts +100 -0
  74. package/src/config/env.ts +169 -41
  75. package/src/config/index.ts +28 -20
  76. package/src/config/loader.ts +25 -16
  77. package/src/config/merge.ts +21 -10
  78. package/src/config/types.ts +545 -25
  79. package/src/config/validation.ts +215 -7
  80. package/src/container/forward-ref.ts +22 -22
  81. package/src/container/index.ts +34 -12
  82. package/src/context/index.ts +11 -1
  83. package/src/database/index.ts +7 -190
  84. package/src/database/orm/builder.ts +457 -0
  85. package/src/database/orm/casts/index.ts +130 -0
  86. package/src/database/orm/casts/types.ts +25 -0
  87. package/src/database/orm/compiler.ts +304 -0
  88. package/src/database/orm/hooks/index.ts +114 -0
  89. package/src/database/orm/index.ts +61 -0
  90. package/src/database/orm/model-registry.ts +59 -0
  91. package/src/database/orm/model.ts +821 -0
  92. package/src/database/orm/relationships/base.ts +146 -0
  93. package/src/database/orm/relationships/belongs-to-many.ts +179 -0
  94. package/src/database/orm/relationships/belongs-to.ts +56 -0
  95. package/src/database/orm/relationships/has-many.ts +45 -0
  96. package/src/database/orm/relationships/has-one.ts +41 -0
  97. package/src/database/orm/relationships/index.ts +11 -0
  98. package/src/database/orm/scopes/index.ts +55 -0
  99. package/src/events/__tests__/event-system.test.ts +235 -0
  100. package/src/events/config.ts +238 -0
  101. package/src/events/example-usage.ts +185 -0
  102. package/src/events/index.ts +278 -0
  103. package/src/events/manager.ts +385 -0
  104. package/src/events/registry.ts +182 -0
  105. package/src/events/types.ts +124 -0
  106. package/src/frontend/api-routes.ts +65 -23
  107. package/src/frontend/bundler.ts +76 -34
  108. package/src/frontend/console-client.ts +2 -2
  109. package/src/frontend/console-stream.ts +94 -38
  110. package/src/frontend/dev-server.ts +94 -46
  111. package/src/frontend/file-router.ts +61 -19
  112. package/src/frontend/frameworks/index.ts +37 -10
  113. package/src/frontend/frameworks/react.ts +10 -8
  114. package/src/frontend/frameworks/solid.ts +11 -9
  115. package/src/frontend/frameworks/svelte.ts +15 -9
  116. package/src/frontend/frameworks/vue.ts +13 -11
  117. package/src/frontend/hmr-client.ts +12 -10
  118. package/src/frontend/hmr.ts +146 -103
  119. package/src/frontend/index.ts +14 -5
  120. package/src/frontend/islands.ts +41 -22
  121. package/src/frontend/isr.ts +59 -37
  122. package/src/frontend/layout.ts +36 -21
  123. package/src/frontend/ssr/react.ts +74 -27
  124. package/src/frontend/ssr/solid.ts +54 -20
  125. package/src/frontend/ssr/svelte.ts +48 -14
  126. package/src/frontend/ssr/vue.ts +50 -18
  127. package/src/frontend/ssr.ts +83 -39
  128. package/src/frontend/types.ts +91 -56
  129. package/src/health/index.ts +21 -9
  130. package/src/i18n/engine.ts +305 -0
  131. package/src/i18n/index.ts +38 -0
  132. package/src/i18n/loader.ts +218 -0
  133. package/src/i18n/middleware.ts +164 -0
  134. package/src/i18n/negotiator.ts +162 -0
  135. package/src/i18n/types.ts +158 -0
  136. package/src/index.ts +179 -27
  137. package/src/jobs/drivers/memory.ts +315 -0
  138. package/src/jobs/drivers/redis.ts +459 -0
  139. package/src/jobs/index.ts +30 -0
  140. package/src/jobs/queue.ts +281 -0
  141. package/src/jobs/types.ts +295 -0
  142. package/src/jobs/worker.ts +380 -0
  143. package/src/logger/index.ts +1 -3
  144. package/src/logger/transports/index.ts +62 -22
  145. package/src/metrics/index.ts +25 -16
  146. package/src/migrations/index.ts +9 -0
  147. package/src/modules/filters.ts +13 -17
  148. package/src/modules/guards.ts +49 -26
  149. package/src/modules/index.ts +409 -298
  150. package/src/modules/interceptors.ts +58 -20
  151. package/src/modules/lazy.ts +11 -19
  152. package/src/modules/lifecycle.ts +15 -7
  153. package/src/modules/metadata.ts +15 -5
  154. package/src/modules/pipes.ts +94 -72
  155. package/src/notification/channels/base.ts +68 -0
  156. package/src/notification/channels/email.ts +105 -0
  157. package/src/notification/channels/push.ts +104 -0
  158. package/src/notification/channels/sms.ts +105 -0
  159. package/src/notification/channels/whatsapp.ts +104 -0
  160. package/src/notification/index.ts +48 -0
  161. package/src/notification/service.ts +354 -0
  162. package/src/notification/types.ts +344 -0
  163. package/src/observability/__tests__/observability.test.ts +483 -0
  164. package/src/observability/breadcrumbs.ts +114 -0
  165. package/src/observability/index.ts +136 -0
  166. package/src/observability/interceptor.ts +85 -0
  167. package/src/observability/service.ts +303 -0
  168. package/src/observability/trace.ts +37 -0
  169. package/src/observability/types.ts +196 -0
  170. package/src/openapi/__tests__/decorators.test.ts +335 -0
  171. package/src/openapi/__tests__/document-builder.test.ts +285 -0
  172. package/src/openapi/__tests__/route-scanner.test.ts +334 -0
  173. package/src/openapi/__tests__/schema-generator.test.ts +275 -0
  174. package/src/openapi/decorators.ts +328 -0
  175. package/src/openapi/document-builder.ts +274 -0
  176. package/src/openapi/index.ts +112 -0
  177. package/src/openapi/metadata.ts +112 -0
  178. package/src/openapi/route-scanner.ts +289 -0
  179. package/src/openapi/schema-generator.ts +256 -0
  180. package/src/openapi/swagger-module.ts +166 -0
  181. package/src/openapi/types.ts +398 -0
  182. package/src/orm/index.ts +10 -0
  183. package/src/rpc/index.ts +3 -1
  184. package/src/schema/index.ts +9 -0
  185. package/src/security/index.ts +15 -6
  186. package/src/ssg/index.ts +9 -8
  187. package/src/telemetry/index.ts +76 -22
  188. package/src/template/index.ts +7 -0
  189. package/src/templates/engine.ts +224 -0
  190. package/src/templates/index.ts +9 -0
  191. package/src/templates/loader.ts +331 -0
  192. package/src/templates/renderers/markdown.ts +212 -0
  193. package/src/templates/renderers/simple.ts +269 -0
  194. package/src/templates/types.ts +154 -0
  195. package/src/testing/index.ts +100 -27
  196. package/src/types/optional-deps.d.ts +347 -187
  197. package/src/validation/index.ts +92 -2
  198. package/src/validation/schemas.ts +536 -0
  199. package/tests/integration/fullstack.test.ts +4 -4
  200. package/tests/unit/database.test.ts +2 -72
  201. package/tests/unit/env-validation.test.ts +166 -0
  202. package/tests/unit/events.test.ts +910 -0
  203. package/tests/unit/i18n.test.ts +455 -0
  204. package/tests/unit/jobs.test.ts +493 -0
  205. package/tests/unit/notification.test.ts +988 -0
  206. package/tests/unit/observability.test.ts +453 -0
  207. package/tests/unit/orm/builder.test.ts +323 -0
  208. package/tests/unit/orm/casts.test.ts +179 -0
  209. package/tests/unit/orm/compiler.test.ts +220 -0
  210. package/tests/unit/orm/eager-loading.test.ts +285 -0
  211. package/tests/unit/orm/hooks.test.ts +191 -0
  212. package/tests/unit/orm/model.test.ts +373 -0
  213. package/tests/unit/orm/relationships.test.ts +303 -0
  214. package/tests/unit/orm/scopes.test.ts +74 -0
  215. package/tests/unit/templates-simple.test.ts +53 -0
  216. package/tests/unit/templates.test.ts +454 -0
  217. package/tests/unit/validation.test.ts +18 -24
  218. package/tsconfig.json +11 -3
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Template Loader
3
+ *
4
+ * Loads templates from the filesystem with support for:
5
+ * - YAML front matter (metadata)
6
+ * - Template variants (channel-specific sections)
7
+ * - In-memory caching with TTL
8
+ * - Hot reload in development mode
9
+ */
10
+
11
+ import { readFileSync, watch } from "fs";
12
+ import { existsSync } from "fs";
13
+ import { extname, resolve } from "path";
14
+ import type {
15
+ Template,
16
+ TemplateLoaderOptions,
17
+ TemplateMetadata,
18
+ } from "./types";
19
+
20
+ /**
21
+ * Parses YAML-like front matter from template
22
+ * Format: ---\nkey: value\n---\n
23
+ */
24
+ function parseFrontMatter(content: string): {
25
+ metadata: TemplateMetadata;
26
+ body: string;
27
+ } {
28
+ const lines = content.split("\n");
29
+
30
+ // Check for front matter delimiter
31
+ if (lines[0]?.trim() !== "---") {
32
+ return { metadata: {}, body: content };
33
+ }
34
+
35
+ // Find closing delimiter
36
+ let endIdx = -1;
37
+ for (let i = 1; i < lines.length; i++) {
38
+ if (lines[i]?.trim() === "---") {
39
+ endIdx = i;
40
+ break;
41
+ }
42
+ }
43
+
44
+ if (endIdx === -1) {
45
+ return { metadata: {}, body: content };
46
+ }
47
+
48
+ // Parse front matter
49
+ const frontMatterLines = lines.slice(1, endIdx);
50
+ const metadata: TemplateMetadata = {};
51
+
52
+ for (const line of frontMatterLines) {
53
+ if (!line.trim()) continue;
54
+
55
+ const colonIdx = line.indexOf(":");
56
+ if (colonIdx === -1) continue;
57
+
58
+ const key = line.substring(0, colonIdx).trim();
59
+ let value: unknown = line.substring(colonIdx + 1).trim();
60
+
61
+ // Simple type conversion
62
+ if (value === "true") value = true;
63
+ else if (value === "false") value = false;
64
+ else if (value === "null") value = null;
65
+ else if (!isNaN(Number(value)) && value !== "") value = Number(value);
66
+ else if (value.startsWith("[") && value.endsWith("]")) {
67
+ // Parse simple arrays: [email, sms, push] or ["email", "sms", "push"]
68
+ const arrayContent = value.slice(1, -1).trim();
69
+ value = arrayContent.split(",").map((v) => {
70
+ let item = v.trim();
71
+ // Remove quotes if present
72
+ if (
73
+ (item.startsWith('"') && item.endsWith('"')) ||
74
+ (item.startsWith("'") && item.endsWith("'"))
75
+ ) {
76
+ item = item.slice(1, -1);
77
+ }
78
+ return item;
79
+ });
80
+ }
81
+
82
+ metadata[key] = value;
83
+ }
84
+
85
+ const body = lines.slice(endIdx + 1).join("\n");
86
+ return { metadata, body };
87
+ }
88
+
89
+ /**
90
+ * Parses template variants from sections
91
+ * Format: ## Email\n...\n---\n## SMS\n...\n---\n## Push\n...
92
+ *
93
+ * Supports:
94
+ * - Single variant per section: "## Email"
95
+ * - Multiple variants (slash): "## SMS/Push"
96
+ * - Multiple variants (comma): "## SMS, Push"
97
+ */
98
+ function parseVariants(content: string): Record<string, string> {
99
+ const sections = content.split(/^## /m);
100
+ const variants: Record<string, string> = {};
101
+
102
+ for (const section of sections) {
103
+ if (!section.trim()) continue;
104
+
105
+ const lines = section.split("\n");
106
+ const headerLine = lines[0];
107
+
108
+ if (!headerLine) continue;
109
+
110
+ // Extract variant names from header
111
+ // Supports: "Email", "SMS/Push", "SMS, Push", etc.
112
+ const variantNames = headerLine
113
+ .split(/[,/]\s*/)
114
+ .map((s) => s.trim().toLowerCase())
115
+ .filter((s) => s);
116
+
117
+ // Get section content (skip header and empty lines at start)
118
+ const contentLines = lines.slice(1);
119
+ let contentStart = 0;
120
+ while (
121
+ contentStart < contentLines.length &&
122
+ !contentLines[contentStart]?.trim()
123
+ ) {
124
+ contentStart++;
125
+ }
126
+ const sectionContent = contentLines.slice(contentStart).join("\n").trim();
127
+
128
+ // Handle separator line (---)
129
+ const separatorIdx = sectionContent.indexOf("\n---");
130
+ const finalContent =
131
+ separatorIdx !== -1
132
+ ? sectionContent.substring(0, separatorIdx)
133
+ : sectionContent;
134
+
135
+ // Map all variant names to same content
136
+ for (const variantName of variantNames) {
137
+ variants[variantName] = finalContent;
138
+ }
139
+ }
140
+
141
+ return variants;
142
+ }
143
+
144
+ /**
145
+ * Detects template format from file extension or metadata
146
+ */
147
+ function detectFormat(
148
+ filePath: string,
149
+ metadata: TemplateMetadata,
150
+ ): "markdown" | "text" | "html" {
151
+ // Check metadata override
152
+ if (metadata.format) {
153
+ const fmt = String(metadata.format).toLowerCase();
154
+ if (["markdown", "text", "html"].includes(fmt)) {
155
+ return fmt as "markdown" | "text" | "html";
156
+ }
157
+ }
158
+
159
+ // Detect from extension
160
+ const ext = extname(filePath).toLowerCase();
161
+ if (ext === ".md" || ext === ".markdown") return "markdown";
162
+ if (ext === ".txt" || ext === ".text") return "text";
163
+ if (ext === ".html" || ext === ".htm") return "html";
164
+
165
+ // Default to markdown (most common)
166
+ return "markdown";
167
+ }
168
+
169
+ /**
170
+ * Template loader with caching and hot reload
171
+ */
172
+ export class TemplateLoader {
173
+ private cache: Map<string, { template: Template; timestamp: number }> =
174
+ new Map();
175
+ private watchers: Map<string, ReturnType<typeof watch>> = new Map();
176
+ private metrics = {
177
+ loads: 0,
178
+ cacheHits: 0,
179
+ cacheMisses: 0,
180
+ };
181
+
182
+ constructor(private options: TemplateLoaderOptions) {
183
+ // Set defaults
184
+ this.options.cacheEnabled = options.cacheEnabled ?? true;
185
+ this.options.cacheTtl = options.cacheTtl ?? 3600;
186
+ this.options.maxCacheSize = options.maxCacheSize ?? 100;
187
+ this.options.extension = options.extension ?? ".md";
188
+ }
189
+
190
+ /**
191
+ * Load a template by ID (path like "emails/welcome")
192
+ */
193
+ load(templateId: string): Template {
194
+ const now = Date.now();
195
+
196
+ // Check cache
197
+ if (this.options.cacheEnabled) {
198
+ const cached = this.cache.get(templateId);
199
+ if (cached) {
200
+ const age = (now - cached.timestamp) / 1000;
201
+ if (age < (this.options.cacheTtl ?? 3600)) {
202
+ this.metrics.cacheHits++;
203
+ return cached.template;
204
+ }
205
+ }
206
+ }
207
+
208
+ // Load from disk
209
+ this.metrics.cacheMisses++;
210
+ const template = this._loadFromDisk(templateId);
211
+
212
+ // Cache it
213
+ if (this.options.cacheEnabled) {
214
+ // Prune cache if too large
215
+ if (
216
+ this.cache.size >= (this.options.maxCacheSize ?? 100) &&
217
+ !this.cache.has(templateId)
218
+ ) {
219
+ // Remove oldest entry
220
+ const oldest = Array.from(this.cache.entries()).sort(
221
+ (a, b) => a[1].timestamp - b[1].timestamp,
222
+ )[0];
223
+ if (oldest) {
224
+ this.cache.delete(oldest[0]);
225
+ }
226
+ }
227
+
228
+ this.cache.set(templateId, { template, timestamp: now });
229
+
230
+ // Set up file watcher in development mode
231
+ if (this.options.watch) {
232
+ this._watchTemplate(templateId, template.id);
233
+ }
234
+ }
235
+
236
+ this.metrics.loads++;
237
+ return template;
238
+ }
239
+
240
+ /**
241
+ * Load template from disk with variant parsing
242
+ */
243
+ private _loadFromDisk(templateId: string): Template {
244
+ // Try multiple extensions
245
+ let filePath = resolve(
246
+ this.options.basePath,
247
+ templateId + this.options.extension,
248
+ );
249
+
250
+ if (!existsSync(filePath)) {
251
+ // Try .md if original extension didn't work
252
+ filePath = resolve(this.options.basePath, templateId + ".md");
253
+ }
254
+
255
+ if (!existsSync(filePath)) {
256
+ // Try .txt
257
+ filePath = resolve(this.options.basePath, templateId + ".txt");
258
+ }
259
+
260
+ if (!existsSync(filePath)) {
261
+ throw new Error(`Template not found: ${templateId}`);
262
+ }
263
+
264
+ // Read file
265
+ const content = readFileSync(filePath, "utf-8");
266
+
267
+ // Parse front matter
268
+ const { metadata, body } = parseFrontMatter(content);
269
+
270
+ // Parse variants from body
271
+ const variants = parseVariants(body);
272
+
273
+ // If no variants found, treat entire body as default variant
274
+ if (Object.keys(variants).length === 0) {
275
+ const defaultVariant = metadata.default || "default";
276
+ variants[defaultVariant] = body;
277
+ }
278
+
279
+ // Detect format
280
+ const format = detectFormat(filePath, metadata);
281
+
282
+ return {
283
+ id: templateId,
284
+ format,
285
+ content: body,
286
+ variants,
287
+ metadata,
288
+ loadedAt: Date.now(),
289
+ };
290
+ }
291
+
292
+ /**
293
+ * Watch template file for changes (development mode)
294
+ */
295
+ private _watchTemplate(templateId: string, filePath: string): void {
296
+ if (this.watchers.has(templateId)) {
297
+ return; // Already watching
298
+ }
299
+
300
+ try {
301
+ const fullPath = resolve(this.options.basePath, filePath);
302
+ const watcher = watch(fullPath, () => {
303
+ // Invalidate cache on file change
304
+ this.cache.delete(templateId);
305
+ this.watchers.delete(templateId);
306
+ });
307
+
308
+ this.watchers.set(templateId, watcher);
309
+ } catch {
310
+ // Silently fail if file watching not available
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Clear all caches and watchers
316
+ */
317
+ clear(): void {
318
+ this.cache.clear();
319
+ for (const watcher of this.watchers.values()) {
320
+ watcher.close();
321
+ }
322
+ this.watchers.clear();
323
+ }
324
+
325
+ /**
326
+ * Get loader metrics
327
+ */
328
+ getMetrics() {
329
+ return { ...this.metrics };
330
+ }
331
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Markdown Renderer
3
+ *
4
+ * Converts Markdown to HTML or plain text.
5
+ * Lightweight implementation supporting:
6
+ * - Headings (# ## ###)
7
+ * - Bold/italic (*text*, **text**)
8
+ * - Lists (- item, 1. item)
9
+ * - Links [text](url)
10
+ * - Code (`inline`)
11
+ * - Blockquotes (> quote)
12
+ * - Paragraphs (blank line separated)
13
+ *
14
+ * NOT supported (kept lightweight):
15
+ * - Tables
16
+ * - Code blocks with syntax highlighting
17
+ * - Complex nesting
18
+ * - Strikethrough
19
+ * - Footnotes
20
+ */
21
+
22
+ /**
23
+ * Markdown to HTML renderer
24
+ */
25
+ export class MarkdownRenderer {
26
+ /**
27
+ * Convert Markdown string to HTML
28
+ */
29
+ static toHtml(markdown: string): string {
30
+ let html = markdown;
31
+
32
+ // Escape HTML special characters (but preserve user-intentional HTML)
33
+ // Only escape in content sections (not in already-formed tags)
34
+
35
+ // Process in order of precedence (inner → outer)
36
+
37
+ // 1. Inline code: `code`
38
+ html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
39
+
40
+ // 2. Bold: **text** or __text__
41
+ html = html.replace(/\*\*([^\*]+)\*\*/g, "<strong>$1</strong>");
42
+ html = html.replace(/__([^_]+)__/g, "<strong>$1</strong>");
43
+
44
+ // 3. Italic: *text* or _text_
45
+ // (be careful not to match ** or __)
46
+ html = html.replace(/(?<!\*)\*([^\*]+)\*(?!\*)/g, "<em>$1</em>");
47
+ html = html.replace(/(?<!_)_([^_]+)_(?!_)/g, "<em>$1</em>");
48
+
49
+ // 4. Links: [text](url)
50
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
51
+
52
+ // 5. Line breaks (double space at end of line or \n\n)
53
+ html = html.replace(/ {2}\n/g, "<br>\n");
54
+
55
+ // 6. Blockquotes: > quote
56
+ html = html.replace(/^> (.+)$/gm, "<blockquote>$1</blockquote>");
57
+
58
+ // 7. Headings (must be on own line)
59
+ html = html.replace(/^### ([^\n]+)$/gm, "<h3>$1</h3>");
60
+ html = html.replace(/^## ([^\n]+)$/gm, "<h2>$1</h2>");
61
+ html = html.replace(/^# ([^\n]+)$/gm, "<h1>$1</h1>");
62
+
63
+ // 8. Ordered lists: 1. 2. 3.
64
+ html = this._processOrderedLists(html);
65
+
66
+ // 9. Unordered lists: - * +
67
+ html = this._processUnorderedLists(html);
68
+
69
+ // 10. Paragraphs (blank line = new paragraph)
70
+ html = this._processParagraphs(html);
71
+
72
+ // 11. Horizontal rules: ---
73
+ html = html.replace(/^---$/gm, "<hr>");
74
+
75
+ return html.trim();
76
+ }
77
+
78
+ /**
79
+ * Convert Markdown string to plain text
80
+ */
81
+ static toText(markdown: string): string {
82
+ let text = markdown;
83
+
84
+ // Remove inline code markers: `code` → code
85
+ text = text.replace(/`([^`]+)`/g, "$1");
86
+
87
+ // Remove bold markers: **text** → text
88
+ text = text.replace(/\*\*([^\*]+)\*\*/g, "$1");
89
+ text = text.replace(/__([^_]+)__/g, "$1");
90
+
91
+ // Remove italic markers: *text* → text
92
+ text = text.replace(/\*([^\*]+)\*/g, "$1");
93
+ text = text.replace(/_([^_]+)_/g, "$1");
94
+
95
+ // Convert links: [text](url) → text: url
96
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1: $2");
97
+
98
+ // Remove blockquote markers: > quote → quote
99
+ text = text.replace(/^> (.+)$/gm, "$1");
100
+
101
+ // Remove headings markers: # text → text
102
+ text = text.replace(/^#+\s+(.+)$/gm, "$1");
103
+
104
+ // Remove list markers
105
+ text = text.replace(/^\d+\.\s+/gm, ""); // Ordered: 1. 2. 3.
106
+ text = text.replace(/^[-*+]\s+/gm, ""); // Unordered: - * +
107
+
108
+ // Clean up multiple blank lines
109
+ text = text.replace(/\n\n\n+/g, "\n\n");
110
+
111
+ return text.trim();
112
+ }
113
+
114
+ /**
115
+ * Process ordered lists (1. 2. 3.)
116
+ */
117
+ private static _processOrderedLists(html: string): string {
118
+ // Find list blocks
119
+ const lines = html.split("\n");
120
+ let inList = false;
121
+ let listHtml = "";
122
+ const result: string[] = [];
123
+
124
+ for (let i = 0; i < lines.length; i++) {
125
+ const line = lines[i];
126
+ const match = line.match(/^(\d+)\.\s+(.+)$/);
127
+
128
+ if (match) {
129
+ if (!inList) {
130
+ inList = true;
131
+ listHtml = `<ol>\n<li>${match[2]}</li>`;
132
+ } else {
133
+ listHtml += `\n<li>${match[2]}</li>`;
134
+ }
135
+ } else {
136
+ if (inList) {
137
+ result.push(listHtml + "\n</ol>");
138
+ inList = false;
139
+ listHtml = "";
140
+ }
141
+ result.push(line);
142
+ }
143
+ }
144
+
145
+ if (inList) {
146
+ result.push(listHtml + "\n</ol>");
147
+ }
148
+
149
+ return result.join("\n");
150
+ }
151
+
152
+ /**
153
+ * Process unordered lists (- * +)
154
+ */
155
+ private static _processUnorderedLists(html: string): string {
156
+ // Find list blocks
157
+ const lines = html.split("\n");
158
+ let inList = false;
159
+ let listHtml = "";
160
+ const result: string[] = [];
161
+
162
+ for (let i = 0; i < lines.length; i++) {
163
+ const line = lines[i];
164
+ const match = line.match(/^[-*+]\s+(.+)$/);
165
+
166
+ if (match) {
167
+ if (!inList) {
168
+ inList = true;
169
+ listHtml = `<ul>\n<li>${match[1]}</li>`;
170
+ } else {
171
+ listHtml += `\n<li>${match[1]}</li>`;
172
+ }
173
+ } else {
174
+ if (inList) {
175
+ result.push(listHtml + "\n</ul>");
176
+ inList = false;
177
+ listHtml = "";
178
+ }
179
+ result.push(line);
180
+ }
181
+ }
182
+
183
+ if (inList) {
184
+ result.push(listHtml + "\n</ul>");
185
+ }
186
+
187
+ return result.join("\n");
188
+ }
189
+
190
+ /**
191
+ * Process paragraphs (blank line separated)
192
+ */
193
+ private static _processParagraphs(html: string): string {
194
+ const blocks = html.split(/\n\n+/);
195
+ const result: string[] = [];
196
+
197
+ for (const block of blocks) {
198
+ if (!block.trim()) continue;
199
+
200
+ // Skip if already wrapped in tag
201
+ if (block.match(/^<[a-z]/) || block.match(/^<\/[a-z]/)) {
202
+ result.push(block);
203
+ } else if (block.match(/^<(h[1-6]|ul|ol|blockquote|hr|code)/)) {
204
+ result.push(block);
205
+ } else {
206
+ result.push(`<p>${block}</p>`);
207
+ }
208
+ }
209
+
210
+ return result.join("\n");
211
+ }
212
+ }