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.
- package/LICENSE +21 -0
- package/README.md +422 -0
- package/dist/client/components/Breadcrumbs.d.ts +21 -0
- package/dist/client/components/Breadcrumbs.d.ts.map +1 -0
- package/dist/client/components/Breadcrumbs.js +123 -0
- package/dist/client/components/DocsLayout.d.ts +20 -0
- package/dist/client/components/DocsLayout.d.ts.map +1 -0
- package/dist/client/components/DocsLayout.js +387 -0
- package/dist/client/components/DocumentContent.d.ts +5 -0
- package/dist/client/components/DocumentContent.d.ts.map +1 -0
- package/dist/client/components/DocumentContent.js +15 -0
- package/dist/client/components/DocumentEdit.d.ts +6 -0
- package/dist/client/components/DocumentEdit.d.ts.map +1 -0
- package/dist/client/components/DocumentEdit.js +153 -0
- package/dist/client/components/DocumentList.d.ts +5 -0
- package/dist/client/components/DocumentList.d.ts.map +1 -0
- package/dist/client/components/DocumentList.js +39 -0
- package/dist/client/components/DocumentProvider.d.ts +42 -0
- package/dist/client/components/DocumentProvider.d.ts.map +1 -0
- package/dist/client/components/DocumentProvider.js +47 -0
- package/dist/client/components/DocumentView.d.ts +6 -0
- package/dist/client/components/DocumentView.d.ts.map +1 -0
- package/dist/client/components/DocumentView.js +58 -0
- package/dist/client/components/DragOverlayItem.d.ts +5 -0
- package/dist/client/components/DragOverlayItem.d.ts.map +1 -0
- package/dist/client/components/DragOverlayItem.js +9 -0
- package/dist/client/components/EmojiPicker.d.ts +8 -0
- package/dist/client/components/EmojiPicker.d.ts.map +1 -0
- package/dist/client/components/EmojiPicker.js +48 -0
- package/dist/client/components/ExportButton.d.ts +22 -0
- package/dist/client/components/ExportButton.d.ts.map +1 -0
- package/dist/client/components/ExportButton.js +97 -0
- package/dist/client/components/Layout.d.ts +7 -0
- package/dist/client/components/Layout.d.ts.map +1 -0
- package/dist/client/components/Layout.js +172 -0
- package/dist/client/components/ReactEmbedDocs.d.ts +8 -0
- package/dist/client/components/ReactEmbedDocs.d.ts.map +1 -0
- package/dist/client/components/ReactEmbedDocs.js +8 -0
- package/dist/client/components/SearchInput.d.ts +2 -0
- package/dist/client/components/SearchInput.d.ts.map +1 -0
- package/dist/client/components/SearchInput.js +7 -0
- package/dist/client/components/Sidebar.d.ts +10 -0
- package/dist/client/components/Sidebar.d.ts.map +1 -0
- package/dist/client/components/Sidebar.js +176 -0
- package/dist/client/components/SortableTreeItem.d.ts +13 -0
- package/dist/client/components/SortableTreeItem.d.ts.map +1 -0
- package/dist/client/components/SortableTreeItem.js +24 -0
- package/dist/client/components/VersionHistory.d.ts +14 -0
- package/dist/client/components/VersionHistory.d.ts.map +1 -0
- package/dist/client/components/VersionHistory.js +102 -0
- package/dist/client/hooks/useCollaboration.d.ts +99 -0
- package/dist/client/hooks/useCollaboration.d.ts.map +1 -0
- package/dist/client/hooks/useCollaboration.js +180 -0
- package/dist/client/hooks/useDocsQuery.d.ts +84 -0
- package/dist/client/hooks/useDocsQuery.d.ts.map +1 -0
- package/dist/client/hooks/useDocsQuery.js +241 -0
- package/dist/client/hooks/useExport.d.ts +31 -0
- package/dist/client/hooks/useExport.d.ts.map +1 -0
- package/dist/client/hooks/useExport.js +66 -0
- package/dist/client/hooks/useFileUpload.d.ts +44 -0
- package/dist/client/hooks/useFileUpload.d.ts.map +1 -0
- package/dist/client/hooks/useFileUpload.js +193 -0
- package/dist/client/hooks/useSystemTheme.d.ts +2 -0
- package/dist/client/hooks/useSystemTheme.d.ts.map +1 -0
- package/dist/client/hooks/useSystemTheme.js +19 -0
- package/dist/client/hooks/useVersions.d.ts +105 -0
- package/dist/client/hooks/useVersions.d.ts.map +1 -0
- package/dist/client/hooks/useVersions.js +129 -0
- package/dist/client/index.d.ts +23 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +18 -0
- package/dist/client/lib/blocknoteTheme.d.ts +13 -0
- package/dist/client/lib/blocknoteTheme.d.ts.map +1 -0
- package/dist/client/lib/blocknoteTheme.js +76 -0
- package/dist/client/lib/path.d.ts +8 -0
- package/dist/client/lib/path.d.ts.map +1 -0
- package/dist/client/lib/path.js +30 -0
- package/dist/client/providers/DocumentProvider.d.ts +1 -0
- package/dist/client/providers/DocumentProvider.d.ts.map +1 -0
- package/dist/client/providers/DocumentProvider.js +1 -0
- package/dist/server/CollaborationService.d.ts +134 -0
- package/dist/server/CollaborationService.d.ts.map +1 -0
- package/dist/server/CollaborationService.js +307 -0
- package/dist/server/DocsService.d.ts +115 -0
- package/dist/server/DocsService.d.ts.map +1 -0
- package/dist/server/DocsService.js +512 -0
- package/dist/server/ExportService.d.ts +106 -0
- package/dist/server/ExportService.d.ts.map +1 -0
- package/dist/server/ExportService.js +501 -0
- package/dist/server/FilesService.d.ts +44 -0
- package/dist/server/FilesService.d.ts.map +1 -0
- package/dist/server/FilesService.js +78 -0
- package/dist/server/VersioningService.d.ts +112 -0
- package/dist/server/VersioningService.d.ts.map +1 -0
- package/dist/server/VersioningService.js +264 -0
- package/dist/server/db.d.ts +7 -0
- package/dist/server/db.d.ts.map +1 -0
- package/dist/server/db.js +22 -0
- package/dist/server/index.d.ts +55 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +36 -0
- package/dist/server/routes.d.ts +9 -0
- package/dist/server/routes.d.ts.map +1 -0
- package/dist/server/routes.js +483 -0
- package/dist/server/schema.d.ts +587 -0
- package/dist/server/schema.d.ts.map +1 -0
- package/dist/server/schema.js +126 -0
- package/dist/shared/types.d.ts +314 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +48 -0
- package/drizzle/migrations/0000_gray_monster_badoon.sql +88 -0
- package/drizzle/migrations/meta/0000_snapshot.json +574 -0
- package/drizzle/migrations/meta/_journal.json +13 -0
- package/package.json +109 -0
- 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 ``;
|
|
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, '&')
|
|
486
|
+
.replace(/</g, '<')
|
|
487
|
+
.replace(/>/g, '>')
|
|
488
|
+
.replace(/"/g, '"')
|
|
489
|
+
.replace(/'/g, ''');
|
|
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
|
+
}
|