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.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.rubocop.yml +8 -0
- data/.yardopts +11 -0
- data/CLAUDE.md +141 -0
- data/README.md +409 -0
- data/Rakefile +19 -0
- data/app/assets/javascripts/inkpen/controllers/editor_controller.js +2050 -0
- data/app/assets/javascripts/inkpen/controllers/sticky_toolbar_controller.js +667 -0
- data/app/assets/javascripts/inkpen/controllers/toolbar_controller.js +693 -0
- data/app/assets/javascripts/inkpen/export/html.js +637 -0
- data/app/assets/javascripts/inkpen/export/index.js +30 -0
- data/app/assets/javascripts/inkpen/export/markdown.js +697 -0
- data/app/assets/javascripts/inkpen/export/pdf.js +372 -0
- data/app/assets/javascripts/inkpen/extensions/advanced_table.js +640 -0
- data/app/assets/javascripts/inkpen/extensions/block_commands.js +300 -0
- data/app/assets/javascripts/inkpen/extensions/block_gutter.js +338 -0
- data/app/assets/javascripts/inkpen/extensions/callout.js +303 -0
- data/app/assets/javascripts/inkpen/extensions/columns.js +403 -0
- data/app/assets/javascripts/inkpen/extensions/database.js +990 -0
- data/app/assets/javascripts/inkpen/extensions/document_section.js +352 -0
- data/app/assets/javascripts/inkpen/extensions/drag_handle.js +407 -0
- data/app/assets/javascripts/inkpen/extensions/embed.js +629 -0
- data/app/assets/javascripts/inkpen/extensions/enhanced_image.js +566 -0
- data/app/assets/javascripts/inkpen/extensions/export_commands.js +271 -0
- data/app/assets/javascripts/inkpen/extensions/file_attachment.js +593 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/index.js +58 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table.js +638 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_cell.js +100 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/inkpen_table_header.js +100 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_constants.js +152 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_helpers.js +254 -0
- data/app/assets/javascripts/inkpen/extensions/inkpen_table/table_menu.js +282 -0
- data/app/assets/javascripts/inkpen/extensions/preformatted.js +239 -0
- data/app/assets/javascripts/inkpen/extensions/section.js +281 -0
- data/app/assets/javascripts/inkpen/extensions/section_title.js +126 -0
- data/app/assets/javascripts/inkpen/extensions/slash_commands.js +439 -0
- data/app/assets/javascripts/inkpen/extensions/table_of_contents.js +474 -0
- data/app/assets/javascripts/inkpen/extensions/toggle_block.js +332 -0
- data/app/assets/javascripts/inkpen/index.js +87 -0
- data/app/assets/stylesheets/inkpen/advanced_table.css +514 -0
- data/app/assets/stylesheets/inkpen/animations.css +626 -0
- data/app/assets/stylesheets/inkpen/block_gutter.css +265 -0
- data/app/assets/stylesheets/inkpen/callout.css +359 -0
- data/app/assets/stylesheets/inkpen/columns.css +314 -0
- data/app/assets/stylesheets/inkpen/database.css +658 -0
- data/app/assets/stylesheets/inkpen/document_section.css +305 -0
- data/app/assets/stylesheets/inkpen/drag_drop.css +220 -0
- data/app/assets/stylesheets/inkpen/editor.css +652 -0
- data/app/assets/stylesheets/inkpen/embed.css +468 -0
- data/app/assets/stylesheets/inkpen/enhanced_image.css +453 -0
- data/app/assets/stylesheets/inkpen/export.css +499 -0
- data/app/assets/stylesheets/inkpen/file_attachment.css +347 -0
- data/app/assets/stylesheets/inkpen/footnotes.css +136 -0
- data/app/assets/stylesheets/inkpen/inkpen_table.css +608 -0
- data/app/assets/stylesheets/inkpen/preformatted.css +215 -0
- data/app/assets/stylesheets/inkpen/search_replace.css +58 -0
- data/app/assets/stylesheets/inkpen/section.css +236 -0
- data/app/assets/stylesheets/inkpen/slash_menu.css +252 -0
- data/app/assets/stylesheets/inkpen/sticky_toolbar.css +314 -0
- data/app/assets/stylesheets/inkpen/toc.css +386 -0
- data/app/assets/stylesheets/inkpen/toggle.css +260 -0
- data/app/helpers/inkpen/editor_helper.rb +114 -0
- data/app/views/inkpen/_editor.html.erb +139 -0
- data/config/importmap.rb +170 -0
- data/docs/.DS_Store +0 -0
- data/docs/CHANGELOG.md +571 -0
- data/docs/FEATURES.md +436 -0
- data/docs/ROADMAP.md +3029 -0
- data/docs/VISION.md +235 -0
- data/docs/extensions/INKPEN_TABLE.md +482 -0
- data/docs/thinking/CORRECTED_NO_VUE.md +756 -0
- data/docs/thinking/EXECUTIVE_SUMMARY.md +403 -0
- data/docs/thinking/INKPEN_CODE_SAMPLES.md +1479 -0
- data/docs/thinking/INKPEN_MASTER_GUIDE.md +891 -0
- data/docs/thinking/README_START_HERE.md +341 -0
- data/lib/inkpen/configuration.rb +175 -0
- data/lib/inkpen/editor.rb +204 -0
- data/lib/inkpen/engine.rb +32 -0
- data/lib/inkpen/extensions/base.rb +109 -0
- data/lib/inkpen/extensions/code_block_syntax.rb +177 -0
- data/lib/inkpen/extensions/document_section.rb +111 -0
- data/lib/inkpen/extensions/forced_document.rb +183 -0
- data/lib/inkpen/extensions/mention.rb +155 -0
- data/lib/inkpen/extensions/preformatted.rb +111 -0
- data/lib/inkpen/extensions/section.rb +139 -0
- data/lib/inkpen/extensions/slash_commands.rb +100 -0
- data/lib/inkpen/extensions/table.rb +182 -0
- data/lib/inkpen/extensions/task_list.rb +145 -0
- data/lib/inkpen/sticky_toolbar.rb +157 -0
- data/lib/inkpen/toolbar.rb +145 -0
- data/lib/inkpen/version.rb +5 -0
- data/lib/inkpen.rb +101 -0
- data/sig/inkpen.rbs +4 -0
- 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 `[}/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 = ``
|
|
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, "&")
|
|
600
|
+
html = html.replace(/</g, "<")
|
|
601
|
+
html = html.replace(/>/g, ">")
|
|
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
|
+
}
|