inkpen 0.7.1

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 (95) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.rubocop.yml +8 -0
  4. data/.yardopts +11 -0
  5. data/CLAUDE.md +141 -0
  6. data/README.md +409 -0
  7. data/Rakefile +19 -0
  8. data/app/assets/javascripts/inkpen/controllers/editor_controller.js +2050 -0
  9. data/app/assets/javascripts/inkpen/controllers/sticky_toolbar_controller.js +667 -0
  10. data/app/assets/javascripts/inkpen/controllers/toolbar_controller.js +693 -0
  11. data/app/assets/javascripts/inkpen/export/html.js +637 -0
  12. data/app/assets/javascripts/inkpen/export/index.js +30 -0
  13. data/app/assets/javascripts/inkpen/export/markdown.js +697 -0
  14. data/app/assets/javascripts/inkpen/export/pdf.js +372 -0
  15. data/app/assets/javascripts/inkpen/extensions/advanced_table.js +640 -0
  16. data/app/assets/javascripts/inkpen/extensions/block_commands.js +300 -0
  17. data/app/assets/javascripts/inkpen/extensions/block_gutter.js +338 -0
  18. data/app/assets/javascripts/inkpen/extensions/callout.js +303 -0
  19. data/app/assets/javascripts/inkpen/extensions/columns.js +403 -0
  20. data/app/assets/javascripts/inkpen/extensions/database.js +990 -0
  21. data/app/assets/javascripts/inkpen/extensions/document_section.js +352 -0
  22. data/app/assets/javascripts/inkpen/extensions/drag_handle.js +407 -0
  23. data/app/assets/javascripts/inkpen/extensions/embed.js +629 -0
  24. data/app/assets/javascripts/inkpen/extensions/enhanced_image.js +566 -0
  25. data/app/assets/javascripts/inkpen/extensions/export_commands.js +271 -0
  26. data/app/assets/javascripts/inkpen/extensions/file_attachment.js +593 -0
  27. data/app/assets/javascripts/inkpen/extensions/inkpen_table/index.js +58 -0
  28. data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table.js +638 -0
  29. data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_cell.js +100 -0
  30. data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_header.js +100 -0
  31. data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_constants.js +152 -0
  32. data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_helpers.js +254 -0
  33. data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_menu.js +282 -0
  34. data/app/assets/javascripts/inkpen/extensions/preformatted.js +239 -0
  35. data/app/assets/javascripts/inkpen/extensions/section.js +281 -0
  36. data/app/assets/javascripts/inkpen/extensions/section_title.js +126 -0
  37. data/app/assets/javascripts/inkpen/extensions/slash_commands.js +439 -0
  38. data/app/assets/javascripts/inkpen/extensions/table_of_contents.js +474 -0
  39. data/app/assets/javascripts/inkpen/extensions/toggle_block.js +332 -0
  40. data/app/assets/javascripts/inkpen/index.js +87 -0
  41. data/app/assets/stylesheets/inkpen/advanced_table.css +514 -0
  42. data/app/assets/stylesheets/inkpen/animations.css +626 -0
  43. data/app/assets/stylesheets/inkpen/block_gutter.css +265 -0
  44. data/app/assets/stylesheets/inkpen/callout.css +359 -0
  45. data/app/assets/stylesheets/inkpen/columns.css +314 -0
  46. data/app/assets/stylesheets/inkpen/database.css +658 -0
  47. data/app/assets/stylesheets/inkpen/document_section.css +305 -0
  48. data/app/assets/stylesheets/inkpen/drag_drop.css +220 -0
  49. data/app/assets/stylesheets/inkpen/editor.css +652 -0
  50. data/app/assets/stylesheets/inkpen/embed.css +468 -0
  51. data/app/assets/stylesheets/inkpen/enhanced_image.css +453 -0
  52. data/app/assets/stylesheets/inkpen/export.css +499 -0
  53. data/app/assets/stylesheets/inkpen/file_attachment.css +347 -0
  54. data/app/assets/stylesheets/inkpen/footnotes.css +136 -0
  55. data/app/assets/stylesheets/inkpen/inkpen_table.css +608 -0
  56. data/app/assets/stylesheets/inkpen/preformatted.css +215 -0
  57. data/app/assets/stylesheets/inkpen/search_replace.css +58 -0
  58. data/app/assets/stylesheets/inkpen/section.css +236 -0
  59. data/app/assets/stylesheets/inkpen/slash_menu.css +252 -0
  60. data/app/assets/stylesheets/inkpen/sticky_toolbar.css +314 -0
  61. data/app/assets/stylesheets/inkpen/toc.css +386 -0
  62. data/app/assets/stylesheets/inkpen/toggle.css +260 -0
  63. data/app/helpers/inkpen/editor_helper.rb +114 -0
  64. data/app/views/inkpen/_editor.html.erb +139 -0
  65. data/config/importmap.rb +170 -0
  66. data/docs/.DS_Store +0 -0
  67. data/docs/CHANGELOG.md +571 -0
  68. data/docs/FEATURES.md +436 -0
  69. data/docs/ROADMAP.md +3029 -0
  70. data/docs/VISION.md +235 -0
  71. data/docs/extensions/INKPEN_TABLE.md +482 -0
  72. data/docs/thinking/CORRECTED_NO_VUE.md +756 -0
  73. data/docs/thinking/EXECUTIVE_SUMMARY.md +403 -0
  74. data/docs/thinking/INKPEN_CODE_SAMPLES.md +1479 -0
  75. data/docs/thinking/INKPEN_MASTER_GUIDE.md +891 -0
  76. data/docs/thinking/README_START_HERE.md +341 -0
  77. data/lib/inkpen/configuration.rb +175 -0
  78. data/lib/inkpen/editor.rb +204 -0
  79. data/lib/inkpen/engine.rb +32 -0
  80. data/lib/inkpen/extensions/base.rb +109 -0
  81. data/lib/inkpen/extensions/code_block_syntax.rb +177 -0
  82. data/lib/inkpen/extensions/document_section.rb +111 -0
  83. data/lib/inkpen/extensions/forced_document.rb +183 -0
  84. data/lib/inkpen/extensions/mention.rb +155 -0
  85. data/lib/inkpen/extensions/preformatted.rb +111 -0
  86. data/lib/inkpen/extensions/section.rb +139 -0
  87. data/lib/inkpen/extensions/slash_commands.rb +100 -0
  88. data/lib/inkpen/extensions/table.rb +182 -0
  89. data/lib/inkpen/extensions/task_list.rb +145 -0
  90. data/lib/inkpen/sticky_toolbar.rb +157 -0
  91. data/lib/inkpen/toolbar.rb +145 -0
  92. data/lib/inkpen/version.rb +5 -0
  93. data/lib/inkpen.rb +101 -0
  94. data/sig/inkpen.rbs +4 -0
  95. metadata +165 -0
@@ -0,0 +1,697 @@
1
+ /**
2
+ * Markdown Export/Import
3
+ *
4
+ * Converts TipTap/ProseMirror document to/from GitHub-Flavored Markdown (GFM).
5
+ * Supports frontmatter, tables, task lists, callouts, and custom blocks.
6
+ */
7
+
8
+ // Mark serializers for inline formatting
9
+ const MARK_SERIALIZERS = {
10
+ bold: (text) => `**${text}**`,
11
+ strong: (text) => `**${text}**`,
12
+ italic: (text) => `_${text}_`,
13
+ em: (text) => `_${text}_`,
14
+ strike: (text) => `~~${text}~~`,
15
+ code: (text) => `\`${text}\``,
16
+ link: (text, mark) => `[${text}](${mark.attrs.href}${mark.attrs.title ? ` "${mark.attrs.title}"` : ""})`,
17
+ underline: (text) => `<u>${text}</u>`,
18
+ highlight: (text, mark) => mark.attrs.color
19
+ ? `<mark style="background:${mark.attrs.color}">${text}</mark>`
20
+ : `==${text}==`,
21
+ subscript: (text) => `<sub>${text}</sub>`,
22
+ superscript: (text) => `<sup>${text}</sup>`
23
+ }
24
+
25
+ // Callout type mappings for GFM-style alerts
26
+ const CALLOUT_MAP = {
27
+ info: "NOTE",
28
+ note: "NOTE",
29
+ tip: "TIP",
30
+ warning: "WARNING",
31
+ success: "TIP",
32
+ error: "CAUTION"
33
+ }
34
+
35
+ /**
36
+ * Export editor document to Markdown
37
+ *
38
+ * @param {Object} doc - ProseMirror document
39
+ * @param {Object} options - Export options
40
+ * @returns {string} Markdown content
41
+ */
42
+ export function exportToMarkdown(doc, options = {}) {
43
+ const {
44
+ includeFrontmatter = false,
45
+ frontmatter = {},
46
+ imageStyle = "inline",
47
+ linkStyle = "inline"
48
+ } = options
49
+
50
+ let markdown = ""
51
+
52
+ // Add frontmatter if provided
53
+ if (includeFrontmatter && Object.keys(frontmatter).length > 0) {
54
+ markdown += "---\n"
55
+ markdown += serializeFrontmatter(frontmatter)
56
+ markdown += "---\n\n"
57
+ }
58
+
59
+ // Serialize document content
60
+ markdown += serializeNode(doc, { imageStyle, linkStyle })
61
+
62
+ return markdown.trim() + "\n"
63
+ }
64
+
65
+ /**
66
+ * Import Markdown content to ProseMirror document
67
+ *
68
+ * @param {string} markdown - Markdown content
69
+ * @param {Object} schema - ProseMirror schema
70
+ * @param {Object} options - Import options
71
+ * @returns {Object} { doc, frontmatter }
72
+ */
73
+ export function importFromMarkdown(markdown, schema, options = {}) {
74
+ // Parse frontmatter if present
75
+ const { content, frontmatter } = parseFrontmatter(markdown)
76
+
77
+ // Parse markdown to HTML first (using browser or a simple parser)
78
+ const html = parseMarkdownToHTML(content)
79
+
80
+ // Return structured result
81
+ return {
82
+ html,
83
+ frontmatter
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Serialize frontmatter object to YAML
89
+ */
90
+ function serializeFrontmatter(data) {
91
+ return Object.entries(data)
92
+ .map(([key, value]) => {
93
+ if (typeof value === "string") {
94
+ // Quote strings with special characters
95
+ if (value.includes(":") || value.includes("#") || value.includes("\n")) {
96
+ return `${key}: "${value.replace(/"/g, '\\"')}"`
97
+ }
98
+ return `${key}: ${value}`
99
+ }
100
+ if (Array.isArray(value)) {
101
+ return `${key}:\n${value.map(v => ` - ${v}`).join("\n")}`
102
+ }
103
+ if (typeof value === "object" && value !== null) {
104
+ return `${key}: ${JSON.stringify(value)}`
105
+ }
106
+ return `${key}: ${value}`
107
+ })
108
+ .join("\n") + "\n"
109
+ }
110
+
111
+ /**
112
+ * Parse frontmatter from markdown content
113
+ */
114
+ function parseFrontmatter(markdown) {
115
+ const frontmatterRegex = /^---\n([\s\S]*?)\n---\n/
116
+ const match = markdown.match(frontmatterRegex)
117
+
118
+ if (!match) {
119
+ return { content: markdown, frontmatter: {} }
120
+ }
121
+
122
+ const frontmatterStr = match[1]
123
+ const content = markdown.slice(match[0].length)
124
+
125
+ // Simple YAML parser (handles basic key: value pairs)
126
+ const frontmatter = {}
127
+ const lines = frontmatterStr.split("\n")
128
+
129
+ for (const line of lines) {
130
+ const colonIndex = line.indexOf(":")
131
+ if (colonIndex > 0) {
132
+ const key = line.slice(0, colonIndex).trim()
133
+ let value = line.slice(colonIndex + 1).trim()
134
+
135
+ // Remove quotes if present
136
+ if ((value.startsWith('"') && value.endsWith('"')) ||
137
+ (value.startsWith("'") && value.endsWith("'"))) {
138
+ value = value.slice(1, -1)
139
+ }
140
+
141
+ frontmatter[key] = value
142
+ }
143
+ }
144
+
145
+ return { content, frontmatter }
146
+ }
147
+
148
+ /**
149
+ * Serialize a ProseMirror node to Markdown
150
+ */
151
+ function serializeNode(node, options = {}, depth = 0) {
152
+ if (!node) return ""
153
+
154
+ const type = node.type?.name || node.type
155
+
156
+ switch (type) {
157
+ case "doc":
158
+ return serializeChildren(node, options)
159
+
160
+ case "paragraph":
161
+ return serializeParagraph(node, options) + "\n\n"
162
+
163
+ case "heading":
164
+ return "#".repeat(node.attrs.level) + " " + serializeInlineContent(node) + "\n\n"
165
+
166
+ case "bulletList":
167
+ return serializeList(node, options, "-") + "\n"
168
+
169
+ case "orderedList":
170
+ return serializeOrderedList(node, options) + "\n"
171
+
172
+ case "taskList":
173
+ return serializeTaskList(node, options) + "\n"
174
+
175
+ case "listItem":
176
+ return serializeChildren(node, options)
177
+
178
+ case "taskItem":
179
+ const checked = node.attrs.checked ? "x" : " "
180
+ return `[${checked}] ` + serializeChildren(node, options).trim()
181
+
182
+ case "blockquote":
183
+ return serializeBlockquote(node, options) + "\n"
184
+
185
+ case "codeBlock":
186
+ return serializeCodeBlock(node) + "\n\n"
187
+
188
+ case "preformatted":
189
+ return "```\n" + node.textContent + "\n```\n\n"
190
+
191
+ case "horizontalRule":
192
+ return "---\n\n"
193
+
194
+ case "hardBreak":
195
+ return " \n"
196
+
197
+ case "image":
198
+ case "enhancedImage":
199
+ return serializeImage(node, options) + "\n\n"
200
+
201
+ case "table":
202
+ case "advancedTable":
203
+ return serializeTable(node, options) + "\n"
204
+
205
+ case "callout":
206
+ return serializeCallout(node, options) + "\n"
207
+
208
+ case "toggleBlock":
209
+ return serializeToggle(node, options) + "\n"
210
+
211
+ case "columns":
212
+ return serializeColumns(node, options) + "\n"
213
+
214
+ case "section":
215
+ return serializeChildren(node, options)
216
+
217
+ case "youtube":
218
+ return `[![YouTube](https://img.youtube.com/vi/${extractYoutubeId(node.attrs.src)}/0.jpg)](${node.attrs.src})\n\n`
219
+
220
+ case "embed":
221
+ return `[${node.attrs.provider || "Embed"}](${node.attrs.url})\n\n`
222
+
223
+ case "fileAttachment":
224
+ return `[📎 ${node.attrs.filename}](${node.attrs.url})\n\n`
225
+
226
+ case "tableOfContents":
227
+ return "<!-- Table of Contents -->\n\n"
228
+
229
+ case "database":
230
+ return serializeDatabase(node, options) + "\n"
231
+
232
+ case "text":
233
+ return serializeTextWithMarks(node)
234
+
235
+ default:
236
+ // Fallback: try to serialize children or text content
237
+ if (node.content) {
238
+ return serializeChildren(node, options)
239
+ }
240
+ return node.textContent || ""
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Serialize children nodes
246
+ */
247
+ function serializeChildren(node, options) {
248
+ if (!node.content) return ""
249
+
250
+ let result = ""
251
+ const children = node.content.content || node.content
252
+
253
+ if (Array.isArray(children)) {
254
+ for (const child of children) {
255
+ result += serializeNode(child, options)
256
+ }
257
+ } else if (typeof children.forEach === "function") {
258
+ children.forEach(child => {
259
+ result += serializeNode(child, options)
260
+ })
261
+ }
262
+
263
+ return result
264
+ }
265
+
266
+ /**
267
+ * Serialize paragraph node
268
+ */
269
+ function serializeParagraph(node, options) {
270
+ return serializeInlineContent(node)
271
+ }
272
+
273
+ /**
274
+ * Serialize inline content (text with marks)
275
+ */
276
+ function serializeInlineContent(node) {
277
+ if (!node.content) return ""
278
+
279
+ let result = ""
280
+ const children = node.content.content || node.content
281
+
282
+ const iterate = (items) => {
283
+ if (Array.isArray(items)) {
284
+ for (const item of items) {
285
+ result += serializeTextWithMarks(item)
286
+ }
287
+ } else if (typeof items.forEach === "function") {
288
+ items.forEach(item => {
289
+ result += serializeTextWithMarks(item)
290
+ })
291
+ }
292
+ }
293
+
294
+ iterate(children)
295
+ return result
296
+ }
297
+
298
+ /**
299
+ * Serialize text node with marks
300
+ */
301
+ function serializeTextWithMarks(node) {
302
+ if (!node) return ""
303
+
304
+ let text = node.text || ""
305
+
306
+ if (!node.marks || node.marks.length === 0) {
307
+ return text
308
+ }
309
+
310
+ // Apply marks in order (innermost first)
311
+ for (const mark of node.marks) {
312
+ const serializer = MARK_SERIALIZERS[mark.type?.name || mark.type]
313
+ if (serializer) {
314
+ text = serializer(text, mark)
315
+ }
316
+ }
317
+
318
+ return text
319
+ }
320
+
321
+ /**
322
+ * Serialize bullet/unordered list
323
+ */
324
+ function serializeList(node, options, marker) {
325
+ let result = ""
326
+ const children = node.content?.content || node.content || []
327
+
328
+ const iterate = (items, callback) => {
329
+ if (Array.isArray(items)) {
330
+ items.forEach(callback)
331
+ } else if (typeof items.forEach === "function") {
332
+ items.forEach(callback)
333
+ }
334
+ }
335
+
336
+ iterate(children, (item) => {
337
+ const content = serializeNode(item, options).trim()
338
+ const lines = content.split("\n")
339
+ result += `${marker} ${lines[0]}\n`
340
+ // Indent continuation lines
341
+ for (let i = 1; i < lines.length; i++) {
342
+ if (lines[i].trim()) {
343
+ result += ` ${lines[i]}\n`
344
+ }
345
+ }
346
+ })
347
+
348
+ return result
349
+ }
350
+
351
+ /**
352
+ * Serialize ordered list
353
+ */
354
+ function serializeOrderedList(node, options) {
355
+ let result = ""
356
+ let index = node.attrs?.start || 1
357
+ const children = node.content?.content || node.content || []
358
+
359
+ const iterate = (items, callback) => {
360
+ if (Array.isArray(items)) {
361
+ items.forEach(callback)
362
+ } else if (typeof items.forEach === "function") {
363
+ items.forEach(callback)
364
+ }
365
+ }
366
+
367
+ iterate(children, (item) => {
368
+ const content = serializeNode(item, options).trim()
369
+ const lines = content.split("\n")
370
+ result += `${index}. ${lines[0]}\n`
371
+ // Indent continuation lines
372
+ for (let i = 1; i < lines.length; i++) {
373
+ if (lines[i].trim()) {
374
+ result += ` ${lines[i]}\n`
375
+ }
376
+ }
377
+ index++
378
+ })
379
+
380
+ return result
381
+ }
382
+
383
+ /**
384
+ * Serialize task list
385
+ */
386
+ function serializeTaskList(node, options) {
387
+ let result = ""
388
+ const children = node.content?.content || node.content || []
389
+
390
+ const iterate = (items, callback) => {
391
+ if (Array.isArray(items)) {
392
+ items.forEach(callback)
393
+ } else if (typeof items.forEach === "function") {
394
+ items.forEach(callback)
395
+ }
396
+ }
397
+
398
+ iterate(children, (item) => {
399
+ const content = serializeNode(item, options).trim()
400
+ result += `- ${content}\n`
401
+ })
402
+
403
+ return result
404
+ }
405
+
406
+ /**
407
+ * Serialize blockquote
408
+ */
409
+ function serializeBlockquote(node, options) {
410
+ const content = serializeChildren(node, options).trim()
411
+ return content.split("\n").map(line => `> ${line}`).join("\n") + "\n"
412
+ }
413
+
414
+ /**
415
+ * Serialize code block
416
+ */
417
+ function serializeCodeBlock(node) {
418
+ const language = node.attrs?.language || ""
419
+ return "```" + language + "\n" + node.textContent + "\n```"
420
+ }
421
+
422
+ /**
423
+ * Serialize image
424
+ */
425
+ function serializeImage(node, options) {
426
+ const alt = node.attrs?.alt || ""
427
+ const src = node.attrs?.src || ""
428
+ const title = node.attrs?.title || ""
429
+ const caption = node.attrs?.caption || ""
430
+
431
+ let md = `![${alt}](${src}${title ? ` "${title}"` : ""})`
432
+
433
+ // Add caption as italic text below
434
+ if (caption) {
435
+ md += `\n*${caption}*`
436
+ }
437
+
438
+ return md
439
+ }
440
+
441
+ /**
442
+ * Serialize table to GFM format
443
+ */
444
+ function serializeTable(node, options) {
445
+ const rows = []
446
+ let headerRow = null
447
+ let columnAligns = []
448
+
449
+ const children = node.content?.content || node.content || []
450
+
451
+ const iterate = (items, callback) => {
452
+ if (Array.isArray(items)) {
453
+ items.forEach(callback)
454
+ } else if (typeof items.forEach === "function") {
455
+ items.forEach(callback)
456
+ }
457
+ }
458
+
459
+ iterate(children, (row, rowIndex) => {
460
+ const cells = []
461
+ const rowChildren = row.content?.content || row.content || []
462
+
463
+ iterate(rowChildren, (cell, cellIndex) => {
464
+ const content = serializeInlineContent(cell).trim() || " "
465
+ cells.push(content)
466
+
467
+ // Track alignment from first row cells
468
+ if (rowIndex === 0) {
469
+ const align = cell.attrs?.align || "left"
470
+ columnAligns.push(align)
471
+ }
472
+ })
473
+
474
+ if (rowIndex === 0) {
475
+ // Header row
476
+ headerRow = "| " + cells.join(" | ") + " |"
477
+ rows.push(headerRow)
478
+
479
+ // Separator row with alignment
480
+ const separators = columnAligns.map(align => {
481
+ if (align === "center") return ":---:"
482
+ if (align === "right") return "---:"
483
+ return "---"
484
+ })
485
+ rows.push("| " + separators.join(" | ") + " |")
486
+ } else {
487
+ rows.push("| " + cells.join(" | ") + " |")
488
+ }
489
+ })
490
+
491
+ return rows.join("\n") + "\n"
492
+ }
493
+
494
+ /**
495
+ * Serialize callout to GFM-style alert
496
+ */
497
+ function serializeCallout(node, options) {
498
+ const type = CALLOUT_MAP[node.attrs?.type] || "NOTE"
499
+ const emoji = node.attrs?.emoji || ""
500
+ const content = serializeChildren(node, options).trim()
501
+
502
+ // GFM alert syntax
503
+ let result = `> [!${type}]`
504
+ if (emoji) {
505
+ result += ` ${emoji}`
506
+ }
507
+ result += "\n"
508
+ result += content.split("\n").map(line => `> ${line}`).join("\n")
509
+ result += "\n"
510
+
511
+ return result
512
+ }
513
+
514
+ /**
515
+ * Serialize toggle/details block
516
+ */
517
+ function serializeToggle(node, options) {
518
+ const summary = node.attrs?.summary || "Details"
519
+ const content = serializeChildren(node, options).trim()
520
+
521
+ return `<details>\n<summary>${summary}</summary>\n\n${content}\n\n</details>\n`
522
+ }
523
+
524
+ /**
525
+ * Serialize columns layout
526
+ */
527
+ function serializeColumns(node, options) {
528
+ let result = ""
529
+ const children = node.content?.content || node.content || []
530
+
531
+ const iterate = (items, callback) => {
532
+ if (Array.isArray(items)) {
533
+ items.forEach(callback)
534
+ } else if (typeof items.forEach === "function") {
535
+ items.forEach(callback)
536
+ }
537
+ }
538
+
539
+ iterate(children, (column, index) => {
540
+ if (index > 0) {
541
+ result += "\n---\n\n"
542
+ }
543
+ result += serializeChildren(column, options)
544
+ })
545
+
546
+ return result
547
+ }
548
+
549
+ /**
550
+ * Serialize database to table format
551
+ */
552
+ function serializeDatabase(node, options) {
553
+ const title = node.attrs?.title || "Database"
554
+ const properties = node.attrs?.properties || []
555
+ const rows = node.attrs?.rows || []
556
+
557
+ let result = `**${title}**\n\n`
558
+
559
+ if (properties.length === 0 || rows.length === 0) {
560
+ return result + "*Empty database*\n"
561
+ }
562
+
563
+ // Header row
564
+ result += "| " + properties.map(p => p.name).join(" | ") + " |\n"
565
+ result += "| " + properties.map(() => "---").join(" | ") + " |\n"
566
+
567
+ // Data rows
568
+ for (const row of rows) {
569
+ const cells = properties.map(prop => {
570
+ const value = row[prop.id]
571
+ if (value === null || value === undefined) return ""
572
+ if (typeof value === "boolean") return value ? "✓" : ""
573
+ if (Array.isArray(value)) return value.join(", ")
574
+ return String(value)
575
+ })
576
+ result += "| " + cells.join(" | ") + " |\n"
577
+ }
578
+
579
+ return result
580
+ }
581
+
582
+ /**
583
+ * Extract YouTube video ID from URL
584
+ */
585
+ function extractYoutubeId(url) {
586
+ if (!url) return ""
587
+ const match = url.match(/(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([^&?]+)/)
588
+ return match ? match[1] : ""
589
+ }
590
+
591
+ /**
592
+ * Simple markdown to HTML parser (basic implementation)
593
+ * For production, consider using a proper markdown parser like marked or remark
594
+ */
595
+ function parseMarkdownToHTML(markdown) {
596
+ let html = markdown
597
+
598
+ // Escape HTML
599
+ html = html.replace(/&/g, "&amp;")
600
+ html = html.replace(/</g, "&lt;")
601
+ html = html.replace(/>/g, "&gt;")
602
+
603
+ // Headings
604
+ html = html.replace(/^######\s+(.+)$/gm, "<h6>$1</h6>")
605
+ html = html.replace(/^#####\s+(.+)$/gm, "<h5>$1</h5>")
606
+ html = html.replace(/^####\s+(.+)$/gm, "<h4>$1</h4>")
607
+ html = html.replace(/^###\s+(.+)$/gm, "<h3>$1</h3>")
608
+ html = html.replace(/^##\s+(.+)$/gm, "<h2>$1</h2>")
609
+ html = html.replace(/^#\s+(.+)$/gm, "<h1>$1</h1>")
610
+
611
+ // Bold and italic
612
+ html = html.replace(/\*\*\*(.+?)\*\*\*/g, "<strong><em>$1</em></strong>")
613
+ html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
614
+ html = html.replace(/\*(.+?)\*/g, "<em>$1</em>")
615
+ html = html.replace(/_(.+?)_/g, "<em>$1</em>")
616
+
617
+ // Strikethrough
618
+ html = html.replace(/~~(.+?)~~/g, "<s>$1</s>")
619
+
620
+ // Inline code
621
+ html = html.replace(/`([^`]+)`/g, "<code>$1</code>")
622
+
623
+ // Links
624
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
625
+
626
+ // Images
627
+ html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">')
628
+
629
+ // Horizontal rules
630
+ html = html.replace(/^---$/gm, "<hr>")
631
+ html = html.replace(/^\*\*\*$/gm, "<hr>")
632
+
633
+ // Code blocks
634
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
635
+ return `<pre><code${lang ? ` class="language-${lang}"` : ""}>${code.trim()}</code></pre>`
636
+ })
637
+
638
+ // Blockquotes
639
+ html = html.replace(/^>\s+(.+)$/gm, "<blockquote>$1</blockquote>")
640
+
641
+ // Task lists
642
+ html = html.replace(/^-\s+\[x\]\s+(.+)$/gm, '<li><input type="checkbox" checked disabled> $1</li>')
643
+ html = html.replace(/^-\s+\[\s\]\s+(.+)$/gm, '<li><input type="checkbox" disabled> $1</li>')
644
+
645
+ // Unordered lists
646
+ html = html.replace(/^-\s+(.+)$/gm, "<li>$1</li>")
647
+ html = html.replace(/(<li>.*<\/li>\n?)+/g, "<ul>$&</ul>")
648
+
649
+ // Ordered lists
650
+ html = html.replace(/^\d+\.\s+(.+)$/gm, "<li>$1</li>")
651
+
652
+ // Paragraphs
653
+ html = html.replace(/\n\n+/g, "</p><p>")
654
+ html = "<p>" + html + "</p>"
655
+
656
+ // Clean up empty paragraphs
657
+ html = html.replace(/<p>\s*<\/p>/g, "")
658
+ html = html.replace(/<p>(<h[1-6]>)/g, "$1")
659
+ html = html.replace(/(<\/h[1-6]>)<\/p>/g, "$1")
660
+ html = html.replace(/<p>(<ul>)/g, "$1")
661
+ html = html.replace(/(<\/ul>)<\/p>/g, "$1")
662
+ html = html.replace(/<p>(<blockquote>)/g, "$1")
663
+ html = html.replace(/(<\/blockquote>)<\/p>/g, "$1")
664
+ html = html.replace(/<p>(<pre>)/g, "$1")
665
+ html = html.replace(/(<\/pre>)<\/p>/g, "$1")
666
+ html = html.replace(/<p>(<hr>)<\/p>/g, "$1")
667
+
668
+ return html
669
+ }
670
+
671
+ /**
672
+ * Download markdown as file
673
+ */
674
+ export function downloadMarkdown(content, filename = "document.md") {
675
+ const blob = new Blob([content], { type: "text/markdown;charset=utf-8" })
676
+ const url = URL.createObjectURL(blob)
677
+ const link = document.createElement("a")
678
+ link.href = url
679
+ link.download = filename
680
+ document.body.appendChild(link)
681
+ link.click()
682
+ document.body.removeChild(link)
683
+ URL.revokeObjectURL(url)
684
+ }
685
+
686
+ /**
687
+ * Copy markdown to clipboard
688
+ */
689
+ export async function copyMarkdownToClipboard(content) {
690
+ try {
691
+ await navigator.clipboard.writeText(content)
692
+ return true
693
+ } catch (err) {
694
+ console.error("Failed to copy markdown:", err)
695
+ return false
696
+ }
697
+ }