vibeman 0.0.2 → 0.0.3
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/dist/runtime/api/.tsbuildinfo +1 -1
- package/dist/runtime/api/agent/agent-service.d.ts +7 -6
- package/dist/runtime/api/agent/agent-service.js +36 -27
- package/dist/runtime/api/agent/ai-providers/codex-cli-provider.d.ts +2 -0
- package/dist/runtime/api/agent/ai-providers/codex-cli-provider.js +62 -30
- package/dist/runtime/api/agent/codex-cli-provider.test.js +47 -2
- package/dist/runtime/api/agent/routing-policy.d.ts +13 -30
- package/dist/runtime/api/agent/routing-policy.js +82 -132
- package/dist/runtime/api/agent/routing-policy.test.js +63 -0
- package/dist/runtime/api/api/routers/ai.d.ts +15 -3
- package/dist/runtime/api/api/routers/ai.js +7 -6
- package/dist/runtime/api/api/routers/executions.d.ts +1 -1
- package/dist/runtime/api/api/routers/tasks.d.ts +3 -3
- package/dist/runtime/api/api/routers/workflows.d.ts +8 -0
- package/dist/runtime/api/api/routers/workflows.js +2 -1
- package/dist/runtime/api/api/trpc.d.ts +6 -6
- package/dist/runtime/api/lib/trpc/server.d.ts +27 -7
- package/dist/runtime/api/router.d.ts +27 -7
- package/dist/runtime/api/settings-service.js +49 -1
- package/dist/runtime/api/types/index.d.ts +8 -1
- package/dist/runtime/api/types/settings.d.ts +15 -2
- package/dist/runtime/api/workflows/vibing-orchestrator.js +32 -1
- package/dist/runtime/web/.next/BUILD_ID +1 -1
- package/dist/runtime/web/.next/app-build-manifest.json +18 -11
- package/dist/runtime/web/.next/app-path-routes-manifest.json +2 -1
- package/dist/runtime/web/.next/build-manifest.json +2 -2
- package/dist/runtime/web/.next/prerender-manifest.json +10 -10
- package/dist/runtime/web/.next/routes-manifest.json +8 -0
- package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js +1 -0
- package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route.js.nft.json +1 -0
- package/dist/runtime/web/.next/server/app/.vibeman/assets/images/[...path]/route_client-reference-manifest.js +1 -0
- package/dist/runtime/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/runtime/web/.next/server/app/_not-found.html +2 -2
- package/dist/runtime/web/.next/server/app/_not-found.rsc +1 -1
- package/dist/runtime/web/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
- package/dist/runtime/web/.next/server/app/api/images/[...path]/route.js +1 -1
- package/dist/runtime/web/.next/server/app/api/images/[...path]/route_client-reference-manifest.js +1 -1
- package/dist/runtime/web/.next/server/app/api/upload/route.js +1 -1
- package/dist/runtime/web/.next/server/app/api/upload/route_client-reference-manifest.js +1 -1
- package/dist/runtime/web/.next/server/app/index.html +2 -2
- package/dist/runtime/web/.next/server/app/index.rsc +2 -2
- package/dist/runtime/web/.next/server/app/page.js +21 -21
- package/dist/runtime/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/runtime/web/.next/server/app-paths-manifest.json +2 -1
- package/dist/runtime/web/.next/server/pages/404.html +2 -2
- package/dist/runtime/web/.next/server/pages/500.html +1 -1
- package/dist/runtime/web/.next/server/server-reference-manifest.json +1 -1
- package/dist/runtime/web/.next/static/5_15u1WQCxN1_eHZpldCv/_buildManifest.js +1 -0
- package/dist/runtime/web/.next/static/chunks/{277-0142a939f08738c3.js → 823-6f371a6e829adbba.js} +1 -1
- package/dist/runtime/web/.next/static/chunks/app/.vibeman/assets/images/[...path]/route-751c9265a65409e5.js +1 -0
- package/dist/runtime/web/.next/static/chunks/app/api/health/route-751c9265a65409e5.js +1 -0
- package/dist/runtime/web/.next/static/chunks/app/api/images/[...path]/route-751c9265a65409e5.js +1 -0
- package/dist/runtime/web/.next/static/chunks/app/api/upload/route-751c9265a65409e5.js +1 -0
- package/dist/runtime/web/.next/static/chunks/app/page-9fe7d75095b4ccec.js +1 -0
- package/package.json +1 -1
- package/dist/runtime/api/lib/image-paste-drop-extension.d.ts +0 -26
- package/dist/runtime/api/lib/image-paste-drop-extension.js +0 -125
- package/dist/runtime/api/lib/markdown-utils.d.ts +0 -8
- package/dist/runtime/api/lib/markdown-utils.js +0 -282
- package/dist/runtime/api/lib/markdown-utils.test.js +0 -348
- package/dist/runtime/api/lib/tiptap-utils.clamp-selection.test.d.ts +0 -1
- package/dist/runtime/api/lib/tiptap-utils.clamp-selection.test.js +0 -27
- package/dist/runtime/api/lib/tiptap-utils.d.ts +0 -130
- package/dist/runtime/api/lib/tiptap-utils.js +0 -327
- package/dist/runtime/web/.next/static/chunks/app/api/health/route-105a61ae865ba536.js +0 -1
- package/dist/runtime/web/.next/static/chunks/app/api/images/[...path]/route-105a61ae865ba536.js +0 -1
- package/dist/runtime/web/.next/static/chunks/app/api/upload/route-105a61ae865ba536.js +0 -1
- package/dist/runtime/web/.next/static/chunks/app/page-8c3ba579efc6f918.js +0 -1
- package/dist/runtime/web/.next/static/mRpNgPfbYR_0wrODzlg_4/_buildManifest.js +0 -1
- /package/dist/runtime/api/{lib/markdown-utils.test.d.ts → agent/routing-policy.test.d.ts} +0 -0
- /package/dist/runtime/web/.next/static/{mRpNgPfbYR_0wrODzlg_4 → 5_15u1WQCxN1_eHZpldCv}/_ssgManifest.js +0 -0
package/package.json
CHANGED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { Extension } from '@tiptap/core';
|
|
2
|
-
export interface ImagePasteDropOptions {
|
|
3
|
-
/**
|
|
4
|
-
* Function that handles the image upload process
|
|
5
|
-
*/
|
|
6
|
-
upload?: (file: File, onProgress?: (event: {
|
|
7
|
-
progress: number;
|
|
8
|
-
}) => void, abortSignal?: AbortSignal) => Promise<string>;
|
|
9
|
-
/**
|
|
10
|
-
* Callback for upload errors
|
|
11
|
-
*/
|
|
12
|
-
onError?: (error: Error) => void;
|
|
13
|
-
/**
|
|
14
|
-
* Callback for successful uploads
|
|
15
|
-
*/
|
|
16
|
-
onSuccess?: (url: string) => void;
|
|
17
|
-
/**
|
|
18
|
-
* Maximum file size in bytes
|
|
19
|
-
*/
|
|
20
|
-
maxSize?: number;
|
|
21
|
-
/**
|
|
22
|
-
* Allowed MIME types
|
|
23
|
-
*/
|
|
24
|
-
allowedTypes?: string[];
|
|
25
|
-
}
|
|
26
|
-
export declare const ImagePasteDrop: Extension<ImagePasteDropOptions, any>;
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { Extension } from '@tiptap/core';
|
|
2
|
-
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
3
|
-
import { handleImageUpload } from './tiptap-utils.js';
|
|
4
|
-
export const ImagePasteDrop = Extension.create({
|
|
5
|
-
name: 'imagePasteDrop',
|
|
6
|
-
addOptions() {
|
|
7
|
-
return {
|
|
8
|
-
upload: handleImageUpload,
|
|
9
|
-
onError: undefined,
|
|
10
|
-
onSuccess: undefined,
|
|
11
|
-
maxSize: 5 * 1024 * 1024, // 5MB
|
|
12
|
-
allowedTypes: [
|
|
13
|
-
'image/jpeg',
|
|
14
|
-
'image/jpg',
|
|
15
|
-
'image/png',
|
|
16
|
-
'image/gif',
|
|
17
|
-
'image/webp',
|
|
18
|
-
'image/svg+xml',
|
|
19
|
-
],
|
|
20
|
-
};
|
|
21
|
-
},
|
|
22
|
-
addProseMirrorPlugins() {
|
|
23
|
-
const uploadAndInsertImages = (files, view, position) => {
|
|
24
|
-
files.forEach(async (file) => {
|
|
25
|
-
try {
|
|
26
|
-
// Validate file size
|
|
27
|
-
if (this.options.maxSize && file.size > this.options.maxSize) {
|
|
28
|
-
const error = new Error(`File size exceeds maximum allowed (${this.options.maxSize / (1024 * 1024)}MB)`);
|
|
29
|
-
this.options.onError?.(error);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
// Insert placeholder first
|
|
33
|
-
const placeholderTransaction = view.state.tr.insert(position, view.state.schema.text('🔄 Uploading...'));
|
|
34
|
-
view.dispatch(placeholderTransaction);
|
|
35
|
-
// Upload the image
|
|
36
|
-
const uploadFunction = this.options.upload || handleImageUpload;
|
|
37
|
-
const imageUrl = await uploadFunction(file);
|
|
38
|
-
// Replace placeholder with actual image
|
|
39
|
-
const filename = file.name.replace(/\.[^/.]+$/, '') || 'image';
|
|
40
|
-
const imageNode = view.state.schema.nodes.image.create({
|
|
41
|
-
src: imageUrl,
|
|
42
|
-
alt: filename,
|
|
43
|
-
title: filename,
|
|
44
|
-
});
|
|
45
|
-
// Find the placeholder text and replace it
|
|
46
|
-
const currentState = view.state;
|
|
47
|
-
let found = false;
|
|
48
|
-
currentState.doc.descendants((node, pos) => {
|
|
49
|
-
if (!found && node.isText && node.text === '🔄 Uploading...') {
|
|
50
|
-
const replaceTransaction = currentState.tr.replaceRangeWith(pos, pos + node.nodeSize, imageNode);
|
|
51
|
-
view.dispatch(replaceTransaction);
|
|
52
|
-
found = true;
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
return true;
|
|
56
|
-
});
|
|
57
|
-
this.options.onSuccess?.(imageUrl);
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
// Remove placeholder on error
|
|
61
|
-
const currentState = view.state;
|
|
62
|
-
currentState.doc.descendants((node, pos) => {
|
|
63
|
-
if (node.isText && node.text === '🔄 Uploading...') {
|
|
64
|
-
const deleteTransaction = currentState.tr.delete(pos, pos + node.nodeSize);
|
|
65
|
-
view.dispatch(deleteTransaction);
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
return true;
|
|
69
|
-
});
|
|
70
|
-
const errorMessage = error instanceof Error ? error : new Error('Upload failed');
|
|
71
|
-
this.options.onError?.(errorMessage);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
};
|
|
75
|
-
return [
|
|
76
|
-
new Plugin({
|
|
77
|
-
key: new PluginKey('imagePasteDrop'),
|
|
78
|
-
props: {
|
|
79
|
-
handleDOMEvents: {
|
|
80
|
-
// Handle drag and drop
|
|
81
|
-
drop: (view, event) => {
|
|
82
|
-
event.preventDefault();
|
|
83
|
-
const { files } = event.dataTransfer;
|
|
84
|
-
const imageFiles = Array.from(files).filter((file) => this.options.allowedTypes?.includes(file.type));
|
|
85
|
-
if (imageFiles.length === 0) {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
// Get drop position
|
|
89
|
-
const pos = view.posAtCoords({
|
|
90
|
-
left: event.clientX,
|
|
91
|
-
top: event.clientY,
|
|
92
|
-
});
|
|
93
|
-
if (!pos)
|
|
94
|
-
return false;
|
|
95
|
-
uploadAndInsertImages(imageFiles, view, pos.pos);
|
|
96
|
-
return true;
|
|
97
|
-
},
|
|
98
|
-
// Handle paste
|
|
99
|
-
paste: (view, event) => {
|
|
100
|
-
const { files } = event.clipboardData;
|
|
101
|
-
const imageFiles = Array.from(files).filter((file) => this.options.allowedTypes?.includes(file.type));
|
|
102
|
-
if (imageFiles.length === 0) {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
event.preventDefault();
|
|
106
|
-
// Insert at current cursor position
|
|
107
|
-
const { selection } = view.state;
|
|
108
|
-
uploadAndInsertImages(imageFiles, view, selection.from);
|
|
109
|
-
return true;
|
|
110
|
-
},
|
|
111
|
-
// Prevent default drag behaviors
|
|
112
|
-
dragover: (view, event) => {
|
|
113
|
-
event.preventDefault();
|
|
114
|
-
return false;
|
|
115
|
-
},
|
|
116
|
-
dragenter: (view, event) => {
|
|
117
|
-
event.preventDefault();
|
|
118
|
-
return false;
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
}),
|
|
123
|
-
];
|
|
124
|
-
},
|
|
125
|
-
});
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import { marked } from 'marked';
|
|
2
|
-
import TurndownService from 'turndown';
|
|
3
|
-
import { gfm } from 'turndown-plugin-gfm';
|
|
4
|
-
marked.setOptions({
|
|
5
|
-
gfm: true,
|
|
6
|
-
breaks: false,
|
|
7
|
-
});
|
|
8
|
-
const turndownService = new TurndownService({
|
|
9
|
-
headingStyle: 'atx',
|
|
10
|
-
hr: '---',
|
|
11
|
-
bulletListMarker: '-',
|
|
12
|
-
codeBlockStyle: 'fenced',
|
|
13
|
-
fence: '```',
|
|
14
|
-
});
|
|
15
|
-
// Use GitHub Flavored Markdown plugin
|
|
16
|
-
turndownService.use(gfm);
|
|
17
|
-
// Custom rule for regular list items to use single space after dash
|
|
18
|
-
turndownService.addRule('listItem', {
|
|
19
|
-
filter: (node) => {
|
|
20
|
-
return (node.nodeName === 'LI' && !node.getAttribute('data-type') // Not a task item
|
|
21
|
-
);
|
|
22
|
-
},
|
|
23
|
-
replacement: (content, node, _options) => {
|
|
24
|
-
content = content
|
|
25
|
-
.replace(/^\n+/, '') // Remove leading newlines
|
|
26
|
-
.replace(/\n+$/, '\n') // Replace trailing newlines with just one
|
|
27
|
-
.replace(/\n/gm, '\n '); // Indent subsequent lines
|
|
28
|
-
let prefix = '- '; // Use single space after dash
|
|
29
|
-
const parent = node.parentNode;
|
|
30
|
-
if (parent && parent.nodeName === 'OL') {
|
|
31
|
-
const index = Array.prototype.indexOf.call(parent.children, node);
|
|
32
|
-
const start = parent.getAttribute('start');
|
|
33
|
-
const startIndex = start ? parseInt(start, 10) - 1 : 0;
|
|
34
|
-
prefix = startIndex + index + 1 + '. ';
|
|
35
|
-
}
|
|
36
|
-
return prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '');
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
// Custom rule for TipTap TaskItems
|
|
40
|
-
turndownService.addRule('tiptapTaskItem', {
|
|
41
|
-
filter: (node, _options) => {
|
|
42
|
-
return (node.nodeName === 'LI' &&
|
|
43
|
-
node.getAttribute('data-type') === 'taskItem' &&
|
|
44
|
-
node.getAttribute('data-checked') !== null);
|
|
45
|
-
},
|
|
46
|
-
replacement: (content, node) => {
|
|
47
|
-
const element = node;
|
|
48
|
-
const isChecked = element.getAttribute('data-checked') === 'true';
|
|
49
|
-
const checkbox = isChecked ? '[x]' : '[ ]';
|
|
50
|
-
// Extract text content from the div > p structure
|
|
51
|
-
const contentDiv = element.querySelector('div');
|
|
52
|
-
const textContent = contentDiv ? contentDiv.textContent?.trim() || '' : content.trim();
|
|
53
|
-
return `- ${checkbox} ${textContent}`;
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
// Custom rule for TipTap TaskLists - handles nested structure properly
|
|
57
|
-
turndownService.addRule('tiptapTaskList', {
|
|
58
|
-
filter: (node) => {
|
|
59
|
-
return node.nodeName === 'UL' && node.getAttribute('data-type') === 'taskList';
|
|
60
|
-
},
|
|
61
|
-
replacement: (content, node) => {
|
|
62
|
-
// Process only direct child li elements to preserve nesting
|
|
63
|
-
const directItems = Array.from(node.children).filter((child) => child.nodeName === 'LI' && child.getAttribute('data-type') === 'taskItem');
|
|
64
|
-
if (directItems.length === 0)
|
|
65
|
-
return content;
|
|
66
|
-
const processTaskItem = (li, indent = '') => {
|
|
67
|
-
const isChecked = li.getAttribute('data-checked') === 'true';
|
|
68
|
-
const checkbox = isChecked ? '[x]' : '[ ]';
|
|
69
|
-
// Get text content from the div, excluding nested lists
|
|
70
|
-
const contentDiv = li.querySelector('div');
|
|
71
|
-
let textContent = '';
|
|
72
|
-
if (contentDiv) {
|
|
73
|
-
// Clone the div to avoid modifying the original
|
|
74
|
-
const tempDiv = contentDiv.cloneNode(true);
|
|
75
|
-
// Remove any nested ul elements from the clone to get just the text
|
|
76
|
-
const nestedLists = Array.from(tempDiv.querySelectorAll('ul'));
|
|
77
|
-
nestedLists.forEach((list) => {
|
|
78
|
-
if (list instanceof Element) {
|
|
79
|
-
list.remove();
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
textContent = tempDiv.textContent?.trim() || '';
|
|
83
|
-
}
|
|
84
|
-
let result = `${indent}- ${checkbox} ${textContent}`;
|
|
85
|
-
// Process nested task lists
|
|
86
|
-
const nestedList = li.querySelector('ul[data-type="taskList"]');
|
|
87
|
-
if (nestedList) {
|
|
88
|
-
const nestedItems = Array.from(nestedList.children).filter((child) => child.nodeName === 'LI' && child.getAttribute('data-type') === 'taskItem');
|
|
89
|
-
nestedItems.forEach((nestedLi) => {
|
|
90
|
-
if (nestedLi instanceof Element) {
|
|
91
|
-
result += '\n' + processTaskItem(nestedLi, indent + ' ');
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
return result;
|
|
96
|
-
};
|
|
97
|
-
return '\n' + directItems.map((li) => processTaskItem(li)).join('\n') + '\n';
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
// Override paragraph handling to preserve paragraph breaks
|
|
101
|
-
// Ensure there is a blank line between paragraphs in markdown
|
|
102
|
-
turndownService.addRule('paragraph', {
|
|
103
|
-
filter: 'p',
|
|
104
|
-
replacement: (content) => {
|
|
105
|
-
return '\n\n' + content.trim() + '\n\n';
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
/**
|
|
109
|
-
* Clean up HTML by removing <p> tags within <li> elements under <ul> or <ol> lists
|
|
110
|
-
*/
|
|
111
|
-
function cleanupListParagraphs(html) {
|
|
112
|
-
if (!html?.trim())
|
|
113
|
-
return html;
|
|
114
|
-
// Use DOM parsing to handle nested structures properly
|
|
115
|
-
if (typeof window !== 'undefined') {
|
|
116
|
-
const parser = new DOMParser();
|
|
117
|
-
const doc = parser.parseFromString(html, 'text/html');
|
|
118
|
-
// Find all <li> elements within <ul> or <ol>
|
|
119
|
-
const listItems = doc.querySelectorAll('ul > li, ol > li');
|
|
120
|
-
listItems.forEach((li) => {
|
|
121
|
-
// Find all <p> tags directly within this <li>
|
|
122
|
-
const paragraphs = Array.from(li.querySelectorAll('p'));
|
|
123
|
-
paragraphs.forEach((p) => {
|
|
124
|
-
// Only remove <p> if it's a direct child or within the list item structure
|
|
125
|
-
if (p.parentElement === li ||
|
|
126
|
-
(p.parentElement && ['UL', 'OL', 'LI'].includes(p.parentElement.nodeName))) {
|
|
127
|
-
// Replace <p> with its content
|
|
128
|
-
const content = p.innerHTML;
|
|
129
|
-
const textNode = doc.createDocumentFragment();
|
|
130
|
-
textNode.appendChild(doc.createTextNode(content));
|
|
131
|
-
// For better HTML structure, we'll replace with the content directly
|
|
132
|
-
p.outerHTML = content;
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
return doc.body.innerHTML;
|
|
137
|
-
}
|
|
138
|
-
// Fallback for server-side: use regex approach (less robust but works)
|
|
139
|
-
// Remove <p> and </p> tags within <li> elements
|
|
140
|
-
return html
|
|
141
|
-
.replace(/<li([^>]*)><p>/g, '<li$1>')
|
|
142
|
-
.replace(/<\/p><\/li>/g, '</li>')
|
|
143
|
-
.replace(/<\/p><ul>/g, '<ul>')
|
|
144
|
-
.replace(/<\/p><ol>/g, '<ol>')
|
|
145
|
-
.replace(/<\/ul><p>/g, '</ul>')
|
|
146
|
-
.replace(/<\/ol><p>/g, '</ol>');
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Convert GFM checkbox HTML to Tiptap TaskItem format
|
|
150
|
-
*/
|
|
151
|
-
function convertToTiptapTaskItems(html) {
|
|
152
|
-
if (!html?.trim())
|
|
153
|
-
return html;
|
|
154
|
-
// Use DOM parsing for better accuracy
|
|
155
|
-
if (typeof window !== 'undefined') {
|
|
156
|
-
const parser = new DOMParser();
|
|
157
|
-
const doc = parser.parseFromString(html, 'text/html');
|
|
158
|
-
// Process all ul elements, starting from the deepest nested ones
|
|
159
|
-
const lists = Array.from(doc.querySelectorAll('ul')).reverse();
|
|
160
|
-
lists.forEach((ul) => {
|
|
161
|
-
let hasTaskItems = false;
|
|
162
|
-
// Only process direct children li elements to preserve nesting
|
|
163
|
-
const directListItems = Array.from(ul.children).filter((child) => child.nodeName === 'LI');
|
|
164
|
-
directListItems.forEach((li) => {
|
|
165
|
-
const checkbox = li.querySelector('input[type="checkbox"]');
|
|
166
|
-
if (checkbox) {
|
|
167
|
-
hasTaskItems = true;
|
|
168
|
-
// Get the checkbox state
|
|
169
|
-
const isChecked = checkbox.hasAttribute('checked');
|
|
170
|
-
// Set TaskItem attributes
|
|
171
|
-
li.setAttribute('data-type', 'taskItem');
|
|
172
|
-
li.setAttribute('data-checked', isChecked ? 'true' : 'false');
|
|
173
|
-
// Get the text content, preserving nested structure
|
|
174
|
-
const clonedLi = li.cloneNode(true);
|
|
175
|
-
// Remove the checkbox from the clone
|
|
176
|
-
const clonedCheckbox = clonedLi.querySelector('input[type="checkbox"]');
|
|
177
|
-
if (clonedCheckbox) {
|
|
178
|
-
clonedCheckbox.remove();
|
|
179
|
-
}
|
|
180
|
-
// Extract nested lists to preserve them
|
|
181
|
-
const nestedLists = Array.from(clonedLi.querySelectorAll('ul'));
|
|
182
|
-
const nestedListsHTML = nestedLists.map((list) => list.outerHTML);
|
|
183
|
-
// Remove nested lists temporarily to get just the text content
|
|
184
|
-
nestedLists.forEach((list) => {
|
|
185
|
-
if (list instanceof Element) {
|
|
186
|
-
list.remove();
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
// Get the text content
|
|
190
|
-
const textContent = clonedLi.textContent?.trim() || '';
|
|
191
|
-
// Create the proper TipTap structure
|
|
192
|
-
let innerHTML = `
|
|
193
|
-
<label contenteditable="false">
|
|
194
|
-
<input type="checkbox" ${isChecked ? 'checked' : ''}>
|
|
195
|
-
<span></span>
|
|
196
|
-
</label>
|
|
197
|
-
<div>
|
|
198
|
-
<p>${textContent}</p>`;
|
|
199
|
-
// Add back any nested lists, ensuring they have the correct data-type
|
|
200
|
-
if (nestedListsHTML.length > 0) {
|
|
201
|
-
nestedListsHTML.forEach((nestedHTML) => {
|
|
202
|
-
// Make sure nested task lists have the correct data-type attribute
|
|
203
|
-
let processedHTML = nestedHTML;
|
|
204
|
-
if (nestedHTML.includes('data-type="taskItem"') &&
|
|
205
|
-
!nestedHTML.includes('data-type="taskList"')) {
|
|
206
|
-
processedHTML = nestedHTML.replace('<ul>', '<ul data-type="taskList">');
|
|
207
|
-
}
|
|
208
|
-
innerHTML += processedHTML;
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
innerHTML += '</div>';
|
|
212
|
-
li.innerHTML = innerHTML.trim();
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
// If this ul contains task items, mark it as a taskList
|
|
216
|
-
if (hasTaskItems) {
|
|
217
|
-
ul.setAttribute('data-type', 'taskList');
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
return doc.body.innerHTML;
|
|
221
|
-
}
|
|
222
|
-
// Fallback for server-side: use regex approach (less robust)
|
|
223
|
-
return (html
|
|
224
|
-
// First pass: Convert list items with checkboxes
|
|
225
|
-
.replace(/<li>(\s*)<input\s+([^>]*?)type="checkbox"([^>]*?)>([^<]*)/gi, (match, leadingSpace, beforeType, afterType, textContent) => {
|
|
226
|
-
const isChecked = beforeType.includes('checked') || afterType.includes('checked');
|
|
227
|
-
const cleanText = textContent.trim();
|
|
228
|
-
return `<li data-type="taskItem" data-checked="${isChecked ? 'true' : 'false'}">
|
|
229
|
-
<label contenteditable="false">
|
|
230
|
-
<input type="checkbox" ${isChecked ? 'checked' : ''}>
|
|
231
|
-
<span></span>
|
|
232
|
-
</label>
|
|
233
|
-
<div>
|
|
234
|
-
<p>${cleanText}</p>
|
|
235
|
-
</div>`;
|
|
236
|
-
})
|
|
237
|
-
// Second pass: Mark parent ul as taskList if it contains taskItems
|
|
238
|
-
.replace(/<ul>(\s*(?:<li[^>]*data-type="taskItem"[^>]*>[\s\S]*?<\/li>\s*)+)<\/ul>/gi, '<ul data-type="taskList">$1</ul>'));
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Pre-process markdown to ensure proper task list item boundaries
|
|
242
|
-
*/
|
|
243
|
-
function preprocessMarkdownTaskItems(markdown) {
|
|
244
|
-
if (!markdown?.trim())
|
|
245
|
-
return markdown;
|
|
246
|
-
// Split markdown into lines
|
|
247
|
-
const lines = markdown.split('\n');
|
|
248
|
-
const processedLines = [];
|
|
249
|
-
for (let i = 0; i < lines.length; i++) {
|
|
250
|
-
const line = lines[i];
|
|
251
|
-
const nextLine = i + 1 < lines.length ? lines[i + 1] : '';
|
|
252
|
-
processedLines.push(line);
|
|
253
|
-
// If current line is a task item and next line is not empty and not indented
|
|
254
|
-
// and not another list item, add a blank line to separate them
|
|
255
|
-
if (line.trim().match(/^-\s+\[[ x]\]/) &&
|
|
256
|
-
nextLine.trim() &&
|
|
257
|
-
!nextLine.match(/^-\s+/) &&
|
|
258
|
-
!nextLine.match(/^\s{2,}/)) {
|
|
259
|
-
processedLines.push(''); // Add blank line
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
return processedLines.join('\n');
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Convert markdown to HTML for Tiptap editor
|
|
266
|
-
*/
|
|
267
|
-
export function markdownToHtml(markdown) {
|
|
268
|
-
if (!markdown?.trim())
|
|
269
|
-
return '';
|
|
270
|
-
const preprocessedMarkdown = preprocessMarkdownTaskItems(markdown);
|
|
271
|
-
const html = marked(preprocessedMarkdown);
|
|
272
|
-
return convertToTiptapTaskItems(html);
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Convert HTML to markdown for storage
|
|
276
|
-
*/
|
|
277
|
-
export function htmlToMarkdown(html) {
|
|
278
|
-
if (!html?.trim())
|
|
279
|
-
return '';
|
|
280
|
-
const cleanedHtml = cleanupListParagraphs(html);
|
|
281
|
-
return turndownService.turndown(cleanedHtml);
|
|
282
|
-
}
|