react-embed-docs 0.1.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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +422 -0
  3. package/dist/client/components/Breadcrumbs.d.ts +21 -0
  4. package/dist/client/components/Breadcrumbs.d.ts.map +1 -0
  5. package/dist/client/components/Breadcrumbs.js +123 -0
  6. package/dist/client/components/DocsLayout.d.ts +20 -0
  7. package/dist/client/components/DocsLayout.d.ts.map +1 -0
  8. package/dist/client/components/DocsLayout.js +387 -0
  9. package/dist/client/components/DocumentContent.d.ts +5 -0
  10. package/dist/client/components/DocumentContent.d.ts.map +1 -0
  11. package/dist/client/components/DocumentContent.js +15 -0
  12. package/dist/client/components/DocumentEdit.d.ts +6 -0
  13. package/dist/client/components/DocumentEdit.d.ts.map +1 -0
  14. package/dist/client/components/DocumentEdit.js +153 -0
  15. package/dist/client/components/DocumentList.d.ts +5 -0
  16. package/dist/client/components/DocumentList.d.ts.map +1 -0
  17. package/dist/client/components/DocumentList.js +39 -0
  18. package/dist/client/components/DocumentProvider.d.ts +42 -0
  19. package/dist/client/components/DocumentProvider.d.ts.map +1 -0
  20. package/dist/client/components/DocumentProvider.js +47 -0
  21. package/dist/client/components/DocumentView.d.ts +6 -0
  22. package/dist/client/components/DocumentView.d.ts.map +1 -0
  23. package/dist/client/components/DocumentView.js +58 -0
  24. package/dist/client/components/DragOverlayItem.d.ts +5 -0
  25. package/dist/client/components/DragOverlayItem.d.ts.map +1 -0
  26. package/dist/client/components/DragOverlayItem.js +9 -0
  27. package/dist/client/components/EmojiPicker.d.ts +8 -0
  28. package/dist/client/components/EmojiPicker.d.ts.map +1 -0
  29. package/dist/client/components/EmojiPicker.js +48 -0
  30. package/dist/client/components/ExportButton.d.ts +22 -0
  31. package/dist/client/components/ExportButton.d.ts.map +1 -0
  32. package/dist/client/components/ExportButton.js +97 -0
  33. package/dist/client/components/Layout.d.ts +7 -0
  34. package/dist/client/components/Layout.d.ts.map +1 -0
  35. package/dist/client/components/Layout.js +172 -0
  36. package/dist/client/components/ReactEmbedDocs.d.ts +8 -0
  37. package/dist/client/components/ReactEmbedDocs.d.ts.map +1 -0
  38. package/dist/client/components/ReactEmbedDocs.js +8 -0
  39. package/dist/client/components/SearchInput.d.ts +2 -0
  40. package/dist/client/components/SearchInput.d.ts.map +1 -0
  41. package/dist/client/components/SearchInput.js +7 -0
  42. package/dist/client/components/Sidebar.d.ts +10 -0
  43. package/dist/client/components/Sidebar.d.ts.map +1 -0
  44. package/dist/client/components/Sidebar.js +176 -0
  45. package/dist/client/components/SortableTreeItem.d.ts +13 -0
  46. package/dist/client/components/SortableTreeItem.d.ts.map +1 -0
  47. package/dist/client/components/SortableTreeItem.js +24 -0
  48. package/dist/client/components/VersionHistory.d.ts +14 -0
  49. package/dist/client/components/VersionHistory.d.ts.map +1 -0
  50. package/dist/client/components/VersionHistory.js +102 -0
  51. package/dist/client/hooks/useCollaboration.d.ts +99 -0
  52. package/dist/client/hooks/useCollaboration.d.ts.map +1 -0
  53. package/dist/client/hooks/useCollaboration.js +180 -0
  54. package/dist/client/hooks/useDocsQuery.d.ts +84 -0
  55. package/dist/client/hooks/useDocsQuery.d.ts.map +1 -0
  56. package/dist/client/hooks/useDocsQuery.js +241 -0
  57. package/dist/client/hooks/useExport.d.ts +31 -0
  58. package/dist/client/hooks/useExport.d.ts.map +1 -0
  59. package/dist/client/hooks/useExport.js +66 -0
  60. package/dist/client/hooks/useFileUpload.d.ts +44 -0
  61. package/dist/client/hooks/useFileUpload.d.ts.map +1 -0
  62. package/dist/client/hooks/useFileUpload.js +193 -0
  63. package/dist/client/hooks/useSystemTheme.d.ts +2 -0
  64. package/dist/client/hooks/useSystemTheme.d.ts.map +1 -0
  65. package/dist/client/hooks/useSystemTheme.js +19 -0
  66. package/dist/client/hooks/useVersions.d.ts +105 -0
  67. package/dist/client/hooks/useVersions.d.ts.map +1 -0
  68. package/dist/client/hooks/useVersions.js +129 -0
  69. package/dist/client/index.d.ts +23 -0
  70. package/dist/client/index.d.ts.map +1 -0
  71. package/dist/client/index.js +18 -0
  72. package/dist/client/lib/blocknoteTheme.d.ts +13 -0
  73. package/dist/client/lib/blocknoteTheme.d.ts.map +1 -0
  74. package/dist/client/lib/blocknoteTheme.js +76 -0
  75. package/dist/client/lib/path.d.ts +8 -0
  76. package/dist/client/lib/path.d.ts.map +1 -0
  77. package/dist/client/lib/path.js +30 -0
  78. package/dist/client/providers/DocumentProvider.d.ts +1 -0
  79. package/dist/client/providers/DocumentProvider.d.ts.map +1 -0
  80. package/dist/client/providers/DocumentProvider.js +1 -0
  81. package/dist/server/CollaborationService.d.ts +134 -0
  82. package/dist/server/CollaborationService.d.ts.map +1 -0
  83. package/dist/server/CollaborationService.js +307 -0
  84. package/dist/server/DocsService.d.ts +115 -0
  85. package/dist/server/DocsService.d.ts.map +1 -0
  86. package/dist/server/DocsService.js +512 -0
  87. package/dist/server/ExportService.d.ts +106 -0
  88. package/dist/server/ExportService.d.ts.map +1 -0
  89. package/dist/server/ExportService.js +501 -0
  90. package/dist/server/FilesService.d.ts +44 -0
  91. package/dist/server/FilesService.d.ts.map +1 -0
  92. package/dist/server/FilesService.js +78 -0
  93. package/dist/server/VersioningService.d.ts +112 -0
  94. package/dist/server/VersioningService.d.ts.map +1 -0
  95. package/dist/server/VersioningService.js +264 -0
  96. package/dist/server/db.d.ts +7 -0
  97. package/dist/server/db.d.ts.map +1 -0
  98. package/dist/server/db.js +22 -0
  99. package/dist/server/index.d.ts +55 -0
  100. package/dist/server/index.d.ts.map +1 -0
  101. package/dist/server/index.js +36 -0
  102. package/dist/server/routes.d.ts +9 -0
  103. package/dist/server/routes.d.ts.map +1 -0
  104. package/dist/server/routes.js +483 -0
  105. package/dist/server/schema.d.ts +587 -0
  106. package/dist/server/schema.d.ts.map +1 -0
  107. package/dist/server/schema.js +126 -0
  108. package/dist/shared/types.d.ts +314 -0
  109. package/dist/shared/types.d.ts.map +1 -0
  110. package/dist/shared/types.js +48 -0
  111. package/drizzle/migrations/0000_gray_monster_badoon.sql +88 -0
  112. package/drizzle/migrations/meta/0000_snapshot.json +574 -0
  113. package/drizzle/migrations/meta/_journal.json +13 -0
  114. package/package.json +109 -0
  115. package/styles/docs.css +981 -0
@@ -0,0 +1,501 @@
1
+ /**
2
+ * Service for exporting documents to various formats
3
+ */
4
+ export class ExportService {
5
+ /**
6
+ * Export document to DOCX format
7
+ * Requires 'docx' package to be installed
8
+ */
9
+ async exportToDocx(document, options = {}) {
10
+ const { includeTitle = true, includeEmoji = true } = options;
11
+ try {
12
+ // Dynamic import to avoid bundling docx when not used
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ const docx = await import('docx');
15
+ const { Document, Paragraph, TextRun, HeadingLevel, Packer } = docx;
16
+ const children = [];
17
+ // Add title
18
+ if (includeTitle) {
19
+ const titleText = includeEmoji && document.emoji
20
+ ? `${document.emoji} ${document.title}`
21
+ : document.title;
22
+ children.push(new Paragraph({
23
+ text: titleText,
24
+ heading: HeadingLevel.HEADING_1,
25
+ spacing: { after: 200 },
26
+ }));
27
+ }
28
+ // Convert content blocks to DOCX paragraphs
29
+ for (const block of document.content || []) {
30
+ const paragraph = this.blockToDocxParagraph(block, docx);
31
+ if (paragraph) {
32
+ children.push(paragraph);
33
+ }
34
+ }
35
+ // Create document
36
+ const doc = new Document({
37
+ sections: [{
38
+ properties: {},
39
+ children,
40
+ }],
41
+ creator: options.author || 'React Embed Docs',
42
+ company: options.company,
43
+ title: document.title,
44
+ });
45
+ // Generate buffer
46
+ const buffer = await Packer.toBuffer(doc);
47
+ return {
48
+ buffer,
49
+ filename: `${this.sanitizeFilename(document.title)}.docx`,
50
+ mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
51
+ };
52
+ }
53
+ catch (error) {
54
+ const errorMessage = error.message || String(error);
55
+ if (errorMessage.includes("Cannot find module 'docx'") || errorMessage.includes("Cannot find package 'docx'")) {
56
+ throw new Error("DOCX export requires the 'docx' package. Install it with: npm install docx");
57
+ }
58
+ throw error;
59
+ }
60
+ }
61
+ /**
62
+ * Export document to PDF format
63
+ * Requires 'puppeteer' or 'playwright' package to be installed
64
+ */
65
+ async exportToPdf(document, options = {}) {
66
+ const { includeTitle = true, includeEmoji = true, format = 'A4' } = options;
67
+ // Generate HTML content
68
+ const html = this.generateHtml(document, { includeTitle, includeEmoji });
69
+ // Try puppeteer first, then playwright
70
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
+ let browser = null;
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ let page = null;
74
+ try {
75
+ // Try puppeteer
76
+ try {
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ const puppeteer = await import('puppeteer');
79
+ browser = await puppeteer.launch({ headless: true });
80
+ }
81
+ catch {
82
+ // Try playwright
83
+ try {
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ const { chromium } = await import('playwright');
86
+ browser = await chromium.launch({ headless: true });
87
+ }
88
+ catch {
89
+ throw new Error("PDF export requires either 'puppeteer' or 'playwright' package. " +
90
+ "Install one with: npm install puppeteer" +
91
+ " or: npm install playwright");
92
+ }
93
+ }
94
+ page = await browser.newPage();
95
+ await page.setContent(html, { waitUntil: 'networkidle0' });
96
+ // Generate PDF
97
+ const pdfBuffer = await page.pdf({
98
+ format,
99
+ printBackground: true,
100
+ margin: {
101
+ top: options.margin?.top || '1in',
102
+ right: options.margin?.right || '1in',
103
+ bottom: options.margin?.bottom || '1in',
104
+ left: options.margin?.left || '1in',
105
+ },
106
+ });
107
+ return {
108
+ buffer: Buffer.from(pdfBuffer),
109
+ filename: `${this.sanitizeFilename(document.title)}.pdf`,
110
+ mimeType: 'application/pdf',
111
+ };
112
+ }
113
+ finally {
114
+ if (browser) {
115
+ await browser.close();
116
+ }
117
+ }
118
+ }
119
+ /**
120
+ * Export document to HTML format
121
+ */
122
+ async exportToHtml(document, options = {}) {
123
+ const { includeTitle = true, includeEmoji = true } = options;
124
+ const html = this.generateHtml(document, { includeTitle, includeEmoji });
125
+ return {
126
+ buffer: Buffer.from(html, 'utf-8'),
127
+ filename: `${this.sanitizeFilename(document.title)}.html`,
128
+ mimeType: 'text/html',
129
+ };
130
+ }
131
+ /**
132
+ * Export document to Markdown format
133
+ */
134
+ async exportToMarkdown(document, options = {}) {
135
+ const { includeTitle = true, includeEmoji = true } = options;
136
+ let markdown = '';
137
+ // Add title
138
+ if (includeTitle) {
139
+ const title = includeEmoji && document.emoji
140
+ ? `${document.emoji} ${document.title}`
141
+ : document.title;
142
+ markdown += `# ${title}\n\n`;
143
+ }
144
+ // Convert blocks to markdown
145
+ for (const block of document.content || []) {
146
+ const blockMd = this.blockToMarkdown(block);
147
+ if (blockMd) {
148
+ markdown += blockMd + '\n\n';
149
+ }
150
+ }
151
+ return {
152
+ buffer: Buffer.from(markdown, 'utf-8'),
153
+ filename: `${this.sanitizeFilename(document.title)}.md`,
154
+ mimeType: 'text/markdown',
155
+ };
156
+ }
157
+ /**
158
+ * Convert a BlockNote block to DOCX Paragraph
159
+ */
160
+ blockToDocxParagraph(block, docx) {
161
+ const { Paragraph, TextRun, HeadingLevel, AlignmentType } = docx;
162
+ switch (block.type) {
163
+ case 'paragraph':
164
+ return new Paragraph({
165
+ children: this.convertContentToTextRuns(block.content, TextRun),
166
+ spacing: { after: 200 },
167
+ });
168
+ case 'heading': {
169
+ const level = block.props?.level || 1;
170
+ const headingLevel = level === 1 ? HeadingLevel.HEADING_1
171
+ : level === 2 ? HeadingLevel.HEADING_2
172
+ : level === 3 ? HeadingLevel.HEADING_3
173
+ : HeadingLevel.HEADING_4;
174
+ return new Paragraph({
175
+ children: this.convertContentToTextRuns(block.content, TextRun),
176
+ heading: headingLevel,
177
+ spacing: { before: 200, after: 100 },
178
+ });
179
+ }
180
+ case 'bulletListItem':
181
+ return new Paragraph({
182
+ children: this.convertContentToTextRuns(block.content, TextRun),
183
+ bullet: { level: 0 },
184
+ spacing: { after: 100 },
185
+ });
186
+ case 'numberedListItem':
187
+ return new Paragraph({
188
+ children: this.convertContentToTextRuns(block.content, TextRun),
189
+ numbering: { reference: 'my-numbering', level: 0 },
190
+ spacing: { after: 100 },
191
+ });
192
+ case 'checkListItem': {
193
+ const checked = block.props?.checked ? '☑ ' : '☐ ';
194
+ return new Paragraph({
195
+ children: [
196
+ new TextRun({ text: checked, bold: true }),
197
+ ...this.convertContentToTextRuns(block.content, TextRun),
198
+ ],
199
+ spacing: { after: 100 },
200
+ });
201
+ }
202
+ case 'quote':
203
+ return new Paragraph({
204
+ children: this.convertContentToTextRuns(block.content, TextRun),
205
+ spacing: { after: 200 },
206
+ border: {
207
+ left: {
208
+ color: '999999',
209
+ space: 20,
210
+ style: 'single',
211
+ size: 24,
212
+ },
213
+ },
214
+ });
215
+ case 'codeBlock': {
216
+ const code = this.extractTextFromContent(block.content);
217
+ return new Paragraph({
218
+ children: [new TextRun({ text: code, font: 'Courier New' })],
219
+ spacing: { after: 200 },
220
+ shading: { fill: 'F5F5F5' },
221
+ });
222
+ }
223
+ default: {
224
+ // Fallback for unknown blocks
225
+ const text = this.extractTextFromContent(block.content);
226
+ if (text) {
227
+ return new Paragraph({
228
+ text,
229
+ spacing: { after: 200 },
230
+ });
231
+ }
232
+ return null;
233
+ }
234
+ }
235
+ }
236
+ /**
237
+ * Convert BlockNote content items to TextRun array
238
+ */
239
+ convertContentToTextRuns(content, TextRun) {
240
+ if (!content)
241
+ return [];
242
+ const runs = [];
243
+ // Flatten nested arrays (BlockNote can have nested content)
244
+ const flatContent = Array.isArray(content[0])
245
+ ? content.flat()
246
+ : content;
247
+ for (const item of flatContent) {
248
+ if (!item)
249
+ continue;
250
+ const styles = {};
251
+ if (item.type === 'text') {
252
+ if (item.bold)
253
+ styles.bold = true;
254
+ if (item.italic)
255
+ styles.italics = true;
256
+ if (item.underline)
257
+ styles.underline = { type: 'single' };
258
+ if (item.strike)
259
+ styles.strike = true;
260
+ if (item.code) {
261
+ styles.font = 'Courier New';
262
+ styles.shading = { fill: 'F5F5F5' };
263
+ }
264
+ if (item.backgroundColor) {
265
+ styles.shading = { fill: item.backgroundColor.replace('#', '') };
266
+ }
267
+ if (item.textColor) {
268
+ styles.color = item.textColor.replace('#', '');
269
+ }
270
+ runs.push(new TextRun({ text: item.text || '', ...styles }));
271
+ }
272
+ }
273
+ return runs.length > 0 ? runs : [new TextRun({ text: '' })];
274
+ }
275
+ /**
276
+ * Extract plain text from content
277
+ */
278
+ extractTextFromContent(content) {
279
+ if (!content)
280
+ return '';
281
+ const flatContent = Array.isArray(content[0])
282
+ ? content.flat()
283
+ : content;
284
+ return flatContent
285
+ .map(item => item?.text || '')
286
+ .join('');
287
+ }
288
+ /**
289
+ * Generate HTML from document
290
+ */
291
+ generateHtml(document, options) {
292
+ const { includeTitle = true, includeEmoji = true } = options;
293
+ let body = '';
294
+ // Add title
295
+ if (includeTitle) {
296
+ const title = includeEmoji && document.emoji
297
+ ? `${document.emoji} ${document.title}`
298
+ : document.title;
299
+ body += `<h1>${this.escapeHtml(title)}</h1>`;
300
+ }
301
+ // Convert blocks to HTML
302
+ for (const block of document.content || []) {
303
+ body += this.blockToHtml(block);
304
+ }
305
+ return `<!DOCTYPE html>
306
+ <html>
307
+ <head>
308
+ <meta charset="UTF-8">
309
+ <title>${this.escapeHtml(document.title)}</title>
310
+ <style>
311
+ body {
312
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
313
+ font-size: 16px;
314
+ line-height: 1.6;
315
+ color: #333;
316
+ max-width: 800px;
317
+ margin: 0 auto;
318
+ padding: 40px;
319
+ }
320
+ h1 { font-size: 2em; margin-bottom: 0.5em; }
321
+ h2 { font-size: 1.5em; margin-top: 1.5em; margin-bottom: 0.5em; }
322
+ h3 { font-size: 1.25em; margin-top: 1.25em; margin-bottom: 0.5em; }
323
+ p { margin-bottom: 1em; }
324
+ ul, ol { margin-bottom: 1em; padding-left: 2em; }
325
+ blockquote {
326
+ border-left: 4px solid #ddd;
327
+ margin: 0 0 1em 0;
328
+ padding-left: 1em;
329
+ color: #666;
330
+ }
331
+ pre {
332
+ background: #f5f5f5;
333
+ padding: 1em;
334
+ border-radius: 4px;
335
+ overflow-x: auto;
336
+ }
337
+ code {
338
+ font-family: 'Consolas', 'Monaco', monospace;
339
+ font-size: 0.9em;
340
+ }
341
+ img { max-width: 100%; height: auto; }
342
+ table { border-collapse: collapse; width: 100%; margin-bottom: 1em; }
343
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
344
+ th { background: #f5f5f5; }
345
+ </style>
346
+ </head>
347
+ <body>
348
+ ${body}
349
+ </body>
350
+ </html>`;
351
+ }
352
+ /**
353
+ * Convert a BlockNote block to HTML
354
+ */
355
+ blockToHtml(block) {
356
+ const content = this.renderContentToHtml(block.content);
357
+ switch (block.type) {
358
+ case 'paragraph':
359
+ return `<p>${content}</p>`;
360
+ case 'heading': {
361
+ const level = block.props?.level || 1;
362
+ return `<h${level}>${content}</h${level}>`;
363
+ }
364
+ case 'bulletListItem':
365
+ return `<ul><li>${content}</li></ul>`;
366
+ case 'numberedListItem':
367
+ return `<ol><li>${content}</li></ol>`;
368
+ case 'checkListItem': {
369
+ const checked = block.props?.checked;
370
+ const checkbox = checked ? '☑' : '☐';
371
+ return `<p>${checkbox} ${content}</p>`;
372
+ }
373
+ case 'quote':
374
+ return `<blockquote>${content}</blockquote>`;
375
+ case 'codeBlock':
376
+ return `<pre><code>${this.escapeHtml(this.extractTextFromContent(block.content))}</code></pre>`;
377
+ case 'image': {
378
+ const url = block.props?.url || '';
379
+ const caption = block.props?.caption || '';
380
+ return `<figure><img src="${this.escapeHtml(url)}" alt="${this.escapeHtml(caption)}"><figcaption>${this.escapeHtml(caption)}</figcaption></figure>`;
381
+ }
382
+ case 'table':
383
+ return this.renderTableToHtml(block);
384
+ case 'divider':
385
+ return '<hr>';
386
+ default:
387
+ return `<p>${content}</p>`;
388
+ }
389
+ }
390
+ /**
391
+ * Render content items to HTML
392
+ */
393
+ renderContentToHtml(content) {
394
+ if (!content)
395
+ return '';
396
+ const flatContent = Array.isArray(content[0])
397
+ ? content.flat()
398
+ : content;
399
+ return flatContent.map(item => this.contentItemToHtml(item)).join('');
400
+ }
401
+ /**
402
+ * Convert a single content item to HTML
403
+ */
404
+ contentItemToHtml(item) {
405
+ if (!item || !item.text)
406
+ return '';
407
+ let text = this.escapeHtml(item.text);
408
+ const styles = item;
409
+ if (styles.bold)
410
+ text = `<strong>${text}</strong>`;
411
+ if (styles.italic)
412
+ text = `<em>${text}</em>`;
413
+ if (styles.underline)
414
+ text = `<u>${text}</u>`;
415
+ if (styles.strike)
416
+ text = `<s>${text}</s>`;
417
+ if (styles.code)
418
+ text = `<code>${text}</code>`;
419
+ if (styles.backgroundColor)
420
+ text = `<span style="background-color: ${styles.backgroundColor}">${text}</span>`;
421
+ if (styles.textColor)
422
+ text = `<span style="color: ${styles.textColor}">${text}</span>`;
423
+ return text;
424
+ }
425
+ /**
426
+ * Convert a table block to HTML
427
+ */
428
+ renderTableToHtml(block) {
429
+ const rows = block.content || [];
430
+ let html = '<table>';
431
+ for (let i = 0; i < rows.length; i++) {
432
+ const row = rows[i];
433
+ html += '<tr>';
434
+ const cells = Array.isArray(row) ? row : [row];
435
+ for (const cell of cells) {
436
+ const tag = i === 0 ? 'th' : 'td';
437
+ const cellContent = this.renderContentToHtml(cell?.content);
438
+ html += `<${tag}>${cellContent}</${tag}>`;
439
+ }
440
+ html += '</tr>';
441
+ }
442
+ html += '</table>';
443
+ return html;
444
+ }
445
+ /**
446
+ * Convert a block to Markdown
447
+ */
448
+ blockToMarkdown(block) {
449
+ const content = this.extractTextFromContent(block.content);
450
+ switch (block.type) {
451
+ case 'paragraph':
452
+ return content;
453
+ case 'heading': {
454
+ const level = block.props?.level || 1;
455
+ return `${'#'.repeat(level)} ${content}`;
456
+ }
457
+ case 'bulletListItem':
458
+ return `- ${content}`;
459
+ case 'numberedListItem':
460
+ return `1. ${content}`;
461
+ case 'checkListItem': {
462
+ const checked = block.props?.checked;
463
+ return `- [${checked ? 'x' : ' '}] ${content}`;
464
+ }
465
+ case 'quote':
466
+ return content.split('\n').map(line => `> ${line}`).join('\n');
467
+ case 'codeBlock':
468
+ return '```\n' + content + '\n```';
469
+ case 'image': {
470
+ const url = block.props?.url || '';
471
+ const caption = block.props?.caption || '';
472
+ return `![${caption}](${url})`;
473
+ }
474
+ case 'divider':
475
+ return '---';
476
+ default:
477
+ return content;
478
+ }
479
+ }
480
+ /**
481
+ * Escape HTML special characters
482
+ */
483
+ escapeHtml(text) {
484
+ return text
485
+ .replace(/&/g, '&amp;')
486
+ .replace(/</g, '&lt;')
487
+ .replace(/>/g, '&gt;')
488
+ .replace(/"/g, '&quot;')
489
+ .replace(/'/g, '&#039;');
490
+ }
491
+ /**
492
+ * Sanitize filename for filesystem
493
+ */
494
+ sanitizeFilename(title) {
495
+ return title
496
+ .replace(/[^a-zA-Z0-9\s-]/g, '')
497
+ .replace(/\s+/g, '-')
498
+ .toLowerCase()
499
+ .substring(0, 50);
500
+ }
501
+ }
@@ -0,0 +1,44 @@
1
+ import type { InsertFileUpload } from '../shared/types.js';
2
+ import { DB } from './db.js';
3
+ import { type File } from './schema.js';
4
+ /**
5
+ * FilesService
6
+ * Handles file uploads, retrieval, and deletion
7
+ * Stores files as base64 encoded strings in the database
8
+ */
9
+ export declare class FilesService {
10
+ private readonly db;
11
+ constructor(db: DB);
12
+ /**
13
+ * Upload a file to the database
14
+ * @param data - File data including filename, mimeType, size, and base64 content
15
+ * @returns The uploaded file record
16
+ * @throws Error if upload fails
17
+ */
18
+ upload(data: InsertFileUpload): Promise<File>;
19
+ /**
20
+ * Get a file by its ID
21
+ * @param id - The file ID
22
+ * @returns The file record or undefined if not found
23
+ */
24
+ getById(id: string): Promise<File | undefined>;
25
+ /**
26
+ * Delete a file by its ID
27
+ * @param id - The file ID to delete
28
+ * @throws Error if deletion fails
29
+ */
30
+ delete(id: string): Promise<void>;
31
+ /**
32
+ * List all files with optional pagination
33
+ * @param options - Pagination options
34
+ * @returns Array of files and total count
35
+ */
36
+ list(options?: {
37
+ limit?: number;
38
+ offset?: number;
39
+ }): Promise<{
40
+ files: File[];
41
+ total: number;
42
+ }>;
43
+ }
44
+ //# sourceMappingURL=FilesService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FilesService.d.ts","sourceRoot":"","sources":["../../src/server/FilesService.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAA;AAC5B,OAAO,EAEL,KAAK,IAAI,EACV,MAAM,aAAa,CAAA;AAEpB;;;;GAIG;AACH,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAEnC;;;;;OAKG;IACG,MAAM,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBnD;;;;OAIG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC;IAQpD;;;;OAIG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvC;;;;OAIG;IACG,IAAI,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAkBzG"}
@@ -0,0 +1,78 @@
1
+ import { eq, sql } from 'drizzle-orm';
2
+ import { nanoid } from 'nanoid';
3
+ import { filesTable } from './schema.js';
4
+ /**
5
+ * FilesService
6
+ * Handles file uploads, retrieval, and deletion
7
+ * Stores files as base64 encoded strings in the database
8
+ */
9
+ export class FilesService {
10
+ db;
11
+ constructor(db) {
12
+ this.db = db;
13
+ }
14
+ /**
15
+ * Upload a file to the database
16
+ * @param data - File data including filename, mimeType, size, and base64 content
17
+ * @returns The uploaded file record
18
+ * @throws Error if upload fails
19
+ */
20
+ async upload(data) {
21
+ const id = nanoid();
22
+ const [result] = await this.db
23
+ .insert(filesTable)
24
+ .values({
25
+ ...data,
26
+ id,
27
+ })
28
+ .returning();
29
+ if (!result) {
30
+ throw new Error('Failed to upload file');
31
+ }
32
+ return result;
33
+ }
34
+ /**
35
+ * Get a file by its ID
36
+ * @param id - The file ID
37
+ * @returns The file record or undefined if not found
38
+ */
39
+ async getById(id) {
40
+ const file = await this.db.query.filesTable.findFirst({
41
+ where: eq(filesTable.id, id),
42
+ });
43
+ return file;
44
+ }
45
+ /**
46
+ * Delete a file by its ID
47
+ * @param id - The file ID to delete
48
+ * @throws Error if deletion fails
49
+ */
50
+ async delete(id) {
51
+ const [result] = await this.db
52
+ .delete(filesTable)
53
+ .where(eq(filesTable.id, id))
54
+ .returning();
55
+ if (!result) {
56
+ throw new Error('File not found');
57
+ }
58
+ }
59
+ /**
60
+ * List all files with optional pagination
61
+ * @param options - Pagination options
62
+ * @returns Array of files and total count
63
+ */
64
+ async list(options = {}) {
65
+ const { limit = 50, offset = 0 } = options;
66
+ const files = await this.db.query.filesTable.findMany({
67
+ limit,
68
+ offset,
69
+ orderBy: (files, { desc }) => [desc(files.createdAt)],
70
+ });
71
+ // Get total count
72
+ const totalResult = await this.db
73
+ .select({ count: sql `count(*)::int` })
74
+ .from(filesTable);
75
+ const total = Number(totalResult[0]?.count ?? 0);
76
+ return { files, total };
77
+ }
78
+ }