mdboard 1.3.0 → 2.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/bin.js +117 -59
- package/index.html +2161 -1579
- package/package.json +7 -5
- package/presets/kanban/api.json +91 -0
- package/presets/kanban/cli.json +69 -0
- package/presets/kanban/docs.json +29 -0
- package/presets/kanban/entities.json +128 -0
- package/presets/kanban/structure.json +15 -0
- package/presets/kanban/ui.json +86 -0
- package/presets/scrum/api.json +98 -0
- package/presets/scrum/cli.json +120 -0
- package/presets/scrum/docs.json +43 -0
- package/presets/scrum/entities.json +268 -0
- package/presets/scrum/structure.json +32 -0
- package/presets/scrum/ui.json +201 -0
- package/presets/shape-up/api.json +40 -0
- package/presets/shape-up/cli.json +44 -0
- package/presets/shape-up/docs.json +32 -0
- package/presets/shape-up/entities.json +140 -0
- package/presets/shape-up/structure.json +28 -0
- package/presets/shape-up/ui.json +114 -0
- package/src/cli/cli.js +186 -210
- package/src/cli/config.js +234 -0
- package/src/cli/init.js +128 -76
- package/src/cli/preset.js +849 -0
- package/src/cli/skill.js +417 -0
- package/src/cli/status.js +126 -96
- package/src/core/config.js +491 -38
- package/src/core/history.js +17 -1
- package/src/core/scanner.js +373 -463
- package/src/core/workspace.js +0 -15
- package/src/server/api.js +464 -741
- package/src/server/server.js +105 -130
- package/build.js +0 -44
- package/defaults.json +0 -43
- package/src/cli/sync.js +0 -194
- package/src/cli/theme.js +0 -142
- package/src/client/app.js +0 -266
- package/src/client/board.js +0 -157
- package/src/client/core.js +0 -331
- package/src/client/editor.js +0 -318
- package/src/client/history.js +0 -137
- package/src/client/metrics.js +0 -38
- package/src/client/milestones.js +0 -77
- package/src/client/notes.js +0 -183
- package/src/client/overview.js +0 -104
- package/src/client/panel.js +0 -637
- package/src/client/styles.css +0 -471
- package/src/client/table.js +0 -111
- package/src/client/template.html +0 -144
- package/src/client/themes.js +0 -261
- package/src/client/workspace.js +0 -164
- package/src/core/agent-scanner.js +0 -260
package/src/client/core.js
DELETED
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
/* ══════════════════════════════════════════════════════════════
|
|
2
|
-
ICON SHAPES — SVG generators by icon name
|
|
3
|
-
══════════════════════════════════════════════════════════════ */
|
|
4
|
-
var ICON_SHAPES = {
|
|
5
|
-
'dashed-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5" stroke-dasharray="3 2"/></svg>'; },
|
|
6
|
-
'circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/></svg>'; },
|
|
7
|
-
'half-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M8 2A6 6 0 0 1 8 14Z" fill="' + c + '"/></svg>'; },
|
|
8
|
-
'three-quarter-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M8 2A6 6 0 1 1 2 8L8 8Z" fill="' + c + '"/></svg>'; },
|
|
9
|
-
'check-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16"><circle cx="8" cy="8" r="7" fill="' + c + '"/><path d="M5 8.5L7 10.5L11 5.5" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>'; },
|
|
10
|
-
'x-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M5.5 5.5L10.5 10.5M10.5 5.5L5.5 10.5" stroke="' + c + '" stroke-width="1.5" stroke-linecap="round"/></svg>'; },
|
|
11
|
-
'slash-circle': function(c) { return '<svg class="icon" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6" stroke="' + c + '" stroke-width="1.5"/><path d="M4.5 11.5L11.5 4.5" stroke="' + c + '" stroke-width="1.5" stroke-linecap="round"/></svg>'; }
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
/* Config-driven maps — populated by initFromConfig() */
|
|
15
|
-
var STATUS_ICON_MAP = {};
|
|
16
|
-
var STATUS_COLOR_MAP = {};
|
|
17
|
-
var ENTITY_NAMES = {};
|
|
18
|
-
|
|
19
|
-
function statusIcon(status) {
|
|
20
|
-
var entry = STATUS_ICON_MAP[status];
|
|
21
|
-
if (entry) {
|
|
22
|
-
var fn = ICON_SHAPES[entry.icon];
|
|
23
|
-
if (fn) return fn(entry.color);
|
|
24
|
-
}
|
|
25
|
-
// Fallback: dashed circle
|
|
26
|
-
return ICON_SHAPES['dashed-circle']('#5A5A63');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function priorityIcon(priority) {
|
|
30
|
-
var c = '#3A3A40', n = 0;
|
|
31
|
-
if (D.config && D.config.priorities) {
|
|
32
|
-
var entry = D.config.priorities.find(function(p) { return p.key === priority; });
|
|
33
|
-
if (entry) { c = entry.color; n = entry.bars; }
|
|
34
|
-
} else {
|
|
35
|
-
switch (priority) {
|
|
36
|
-
case 'urgent': c = '#F97316'; n = 4; break;
|
|
37
|
-
case 'high': c = '#F97316'; n = 3; break;
|
|
38
|
-
case 'medium': c = '#D4A72C'; n = 2; break;
|
|
39
|
-
case 'low': c = '#5B6EF5'; n = 1; break;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
var bars = '';
|
|
43
|
-
for (var i = 0; i < 4; i++) {
|
|
44
|
-
var filled = i < n;
|
|
45
|
-
var h = 3 + i * 3;
|
|
46
|
-
var y = 14 - h;
|
|
47
|
-
bars += '<rect x="' + (1.5 + i * 3.5) + '" y="' + y + '" width="2" height="' + h + '" rx=".5" fill="' + (filled ? c : '#2E2E33') + '"/>';
|
|
48
|
-
}
|
|
49
|
-
return '<svg class="icon" viewBox="0 0 16 16">' + bars + '</svg>';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function milestoneIcon(status) {
|
|
53
|
-
var msStatuses = D.config && D.config.statuses ? D.config.statuses.milestone : null;
|
|
54
|
-
var entry = msStatuses ? msStatuses.find(function(s) { return s.key === status; }) : null;
|
|
55
|
-
var c = entry ? entry.color : '#8B8B93';
|
|
56
|
-
|
|
57
|
-
switch (status) {
|
|
58
|
-
case 'active':
|
|
59
|
-
return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="none" stroke="' + c + '" stroke-width="1.5"/><path d="M8 5L11 8L8 11L5 8Z" fill="' + c + '"/></svg>';
|
|
60
|
-
case 'completed':
|
|
61
|
-
return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="' + c + '"/><path d="M5.5 8L7 9.5L10.5 6" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
62
|
-
default:
|
|
63
|
-
return '<svg class="icon" viewBox="0 0 16 16"><path d="M8 1L15 8L8 15L1 8Z" fill="none" stroke="' + c + '" stroke-width="1.5"/></svg>';
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/* ══════════════════════════════════════════════════════════════
|
|
68
|
-
DATA STORE
|
|
69
|
-
══════════════════════════════════════════════════════════════ */
|
|
70
|
-
var D = {
|
|
71
|
-
config: null, project: null, milestones: [], epics: [], tasks: [],
|
|
72
|
-
sprints: [], allSprints: [], metrics: null, health: null, loaded: false,
|
|
73
|
-
sources: [], activeSource: null, overviewLinks: null, notes: [],
|
|
74
|
-
aiSuggestions: null, theme: null
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
/* ── Helpers ─────────────────────────────────────────────── */
|
|
78
|
-
function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
79
|
-
|
|
80
|
-
function epicColor(name) {
|
|
81
|
-
var colors = ['#5B6EF5','#8B5CF6','#EC4899','#F59E0B','#10B981','#06B6D4','#F97316','#6366F1'];
|
|
82
|
-
var hash = 0;
|
|
83
|
-
for (var i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
84
|
-
return colors[Math.abs(hash) % colors.length];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function badgeClass(prefix, value) {
|
|
88
|
-
if (!value) return prefix + '-backlog';
|
|
89
|
-
return prefix + '-' + value.toLowerCase().replace(/\s+/g, '-');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function fmtDate(d) { return d ? String(d).substring(0, 10) : ''; }
|
|
93
|
-
|
|
94
|
-
function daysRemaining(endDate) {
|
|
95
|
-
if (!endDate) return null;
|
|
96
|
-
var end = new Date(endDate); var now = new Date();
|
|
97
|
-
return Math.ceil((end - now) / 86400000);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
var STATUSES = ['backlog','todo','in-progress','in-review','done','blocked','cancelled'];
|
|
101
|
-
var PRIORITIES = ['urgent','high','medium','low'];
|
|
102
|
-
var STATUS_LABELS = {backlog:'Backlog',todo:'Todo','in-progress':'In Progress','in-review':'In Review',done:'Done',blocked:'Blocked',cancelled:'Cancelled'};
|
|
103
|
-
var PRIORITY_LABELS = {urgent:'Urgent',high:'High',medium:'Medium',low:'Low'};
|
|
104
|
-
var BOARD_COLS = ['backlog','todo','in-progress','in-review','done'];
|
|
105
|
-
var COMPLETED_STATUS = 'done';
|
|
106
|
-
|
|
107
|
-
/* ── Toast ───────────────────────────────────────────────── */
|
|
108
|
-
function showToast(msg, type) {
|
|
109
|
-
var el = document.createElement('div');
|
|
110
|
-
el.className = 'toast toast-' + (type || 'success');
|
|
111
|
-
el.textContent = msg;
|
|
112
|
-
document.body.appendChild(el);
|
|
113
|
-
setTimeout(function() { el.remove(); }, 3000);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/* ── Data Fetching ───────────────────────────────────────── */
|
|
117
|
-
async function fetchJson(url) {
|
|
118
|
-
try { var r = await fetch(url); return r.ok ? await r.json() : null; }
|
|
119
|
-
catch { return null; }
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function patchItem(type, id, updates) {
|
|
123
|
-
try {
|
|
124
|
-
var base = apiBase();
|
|
125
|
-
var r = await fetch(base + '/' + type + '/' + encodeURIComponent(id), {
|
|
126
|
-
method: 'PATCH',
|
|
127
|
-
headers: { 'Content-Type': 'application/json' },
|
|
128
|
-
body: JSON.stringify(updates),
|
|
129
|
-
});
|
|
130
|
-
var data = await r.json();
|
|
131
|
-
if (data && data.ok) {
|
|
132
|
-
showToast('Saved successfully', 'success');
|
|
133
|
-
return true;
|
|
134
|
-
} else {
|
|
135
|
-
showToast('Error: ' + (data.error || 'Unknown'), 'error');
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
} catch (e) {
|
|
139
|
-
showToast('Error: ' + e.message, 'error');
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/* ── CRUD API helpers ─────────────────────────────────────── */
|
|
145
|
-
function apiBase() {
|
|
146
|
-
if (D.activeSource) return '/api/sources/' + encodeURIComponent(D.activeSource);
|
|
147
|
-
return '/api';
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function createItem(collection, data) {
|
|
151
|
-
try {
|
|
152
|
-
var url = apiBase() + '/' + collection;
|
|
153
|
-
var r = await fetch(url, {
|
|
154
|
-
method: 'POST',
|
|
155
|
-
headers: { 'Content-Type': 'application/json' },
|
|
156
|
-
body: JSON.stringify(data),
|
|
157
|
-
});
|
|
158
|
-
var result = await r.json();
|
|
159
|
-
if (result && result.ok) {
|
|
160
|
-
showToast('Created ' + (result.id || ''), 'success');
|
|
161
|
-
return result;
|
|
162
|
-
} else {
|
|
163
|
-
showToast('Error: ' + (result.error || 'Unknown'), 'error');
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
} catch (e) {
|
|
167
|
-
showToast('Error: ' + e.message, 'error');
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function deleteItem(collection, id) {
|
|
173
|
-
try {
|
|
174
|
-
var url = apiBase() + '/' + collection + '/' + encodeURIComponent(id);
|
|
175
|
-
var r = await fetch(url, { method: 'DELETE' });
|
|
176
|
-
var result = await r.json();
|
|
177
|
-
if (result && result.ok) {
|
|
178
|
-
showToast('Archived successfully', 'success');
|
|
179
|
-
return true;
|
|
180
|
-
} else {
|
|
181
|
-
showToast('Error: ' + (result.error || 'Unknown'), 'error');
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
} catch (e) {
|
|
185
|
-
showToast('Error: ' + e.message, 'error');
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/* ── Data Loading ────────────────────────────────────────── */
|
|
191
|
-
async function loadAll() {
|
|
192
|
-
var base = apiBase();
|
|
193
|
-
var results = await Promise.all([
|
|
194
|
-
fetchJson(base + '/project'), fetchJson(base + '/milestones'), fetchJson(base + '/epics'),
|
|
195
|
-
fetchJson(base + '/tasks'), fetchJson(base + '/sprint'), fetchJson(base + '/metrics'),
|
|
196
|
-
fetchJson(base + '/health'), fetchJson(base + '/sprints'), fetchJson('/api/config'),
|
|
197
|
-
fetchJson('/api/sources'), fetchJson(base + '/notes')
|
|
198
|
-
]);
|
|
199
|
-
D.project = results[0]; D.milestones = results[1] || []; D.epics = results[2] || [];
|
|
200
|
-
D.tasks = results[3] || [];
|
|
201
|
-
if (results[4]) D.sprints = [results[4]]; else D.sprints = [];
|
|
202
|
-
D.metrics = results[5]; D.health = results[6];
|
|
203
|
-
D.allSprints = results[7] || [];
|
|
204
|
-
D.config = results[8];
|
|
205
|
-
D.sources = results[9] || [];
|
|
206
|
-
D.notes = results[10] || [];
|
|
207
|
-
D.loaded = true;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function refreshData() {
|
|
211
|
-
loadAll().then(function() {
|
|
212
|
-
initFromConfig();
|
|
213
|
-
renderAll();
|
|
214
|
-
if (hasWorkspace()) buildSourceRail();
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function hasWorkspace() {
|
|
219
|
-
return D.config && D.config.workspace && D.config.workspace.sources && D.config.workspace.sources.length > 0;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/* ── Config-driven initialization ────────────────────────── */
|
|
223
|
-
function initFromConfig() {
|
|
224
|
-
if (!D.config) return;
|
|
225
|
-
|
|
226
|
-
var cfg = D.config;
|
|
227
|
-
|
|
228
|
-
// Derive entity names
|
|
229
|
-
ENTITY_NAMES = cfg.entities || {};
|
|
230
|
-
|
|
231
|
-
// Derive statuses, labels, board columns
|
|
232
|
-
if (cfg.statuses && cfg.statuses.task) {
|
|
233
|
-
STATUSES = cfg.statuses.task.map(function(s) { return s.key; });
|
|
234
|
-
STATUS_LABELS = {};
|
|
235
|
-
cfg.statuses.task.forEach(function(s) { STATUS_LABELS[s.key] = s.label; });
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Build icon/color maps for all status types
|
|
239
|
-
STATUS_ICON_MAP = {};
|
|
240
|
-
STATUS_COLOR_MAP = {};
|
|
241
|
-
if (cfg.statuses) {
|
|
242
|
-
Object.keys(cfg.statuses).forEach(function(entityType) {
|
|
243
|
-
cfg.statuses[entityType].forEach(function(s) {
|
|
244
|
-
if (!STATUS_ICON_MAP[s.key]) {
|
|
245
|
-
STATUS_ICON_MAP[s.key] = { icon: s.icon || 'dashed-circle', color: s.color || '#5A5A63' };
|
|
246
|
-
STATUS_COLOR_MAP[s.key] = s.color || '#5A5A63';
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Derive priorities
|
|
253
|
-
if (cfg.priorities) {
|
|
254
|
-
PRIORITIES = cfg.priorities.map(function(p) { return p.key; });
|
|
255
|
-
PRIORITY_LABELS = {};
|
|
256
|
-
cfg.priorities.forEach(function(p) { PRIORITY_LABELS[p.key] = p.label; });
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Board columns
|
|
260
|
-
if (cfg.boardColumns) BOARD_COLS = cfg.boardColumns;
|
|
261
|
-
|
|
262
|
-
// Completed status
|
|
263
|
-
if (cfg.completedStatus) COMPLETED_STATUS = cfg.completedStatus;
|
|
264
|
-
|
|
265
|
-
generateDynamicStyles();
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function generateDynamicStyles() {
|
|
269
|
-
var css = '';
|
|
270
|
-
if (D.config && D.config.statuses && D.config.statuses.task) {
|
|
271
|
-
D.config.statuses.task.forEach(function(s) {
|
|
272
|
-
var key = s.key;
|
|
273
|
-
var color = s.color;
|
|
274
|
-
css += '.badge-' + key + '{background:' + color + '20;color:' + color + '}\n';
|
|
275
|
-
css += '.card[data-status="' + key + '"]{border-left-color:' + color + '}\n';
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
// Milestone/epic/sprint statuses
|
|
279
|
-
['milestone', 'epic', 'sprint'].forEach(function(entityType) {
|
|
280
|
-
if (D.config && D.config.statuses && D.config.statuses[entityType]) {
|
|
281
|
-
D.config.statuses[entityType].forEach(function(s) {
|
|
282
|
-
css += '.badge-' + s.key + '{background:' + s.color + '20;color:' + s.color + '}\n';
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
// Priority badges
|
|
287
|
-
if (D.config && D.config.priorities) {
|
|
288
|
-
D.config.priorities.forEach(function(p) {
|
|
289
|
-
css += '.badge-' + p.key + '{background:' + p.color + '20;color:' + p.color + '}\n';
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
var styleEl = document.getElementById('dynamic-styles');
|
|
293
|
-
if (styleEl) styleEl.textContent = css;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/* ══════════════════════════════════════════════════════════════
|
|
297
|
-
THEME ENGINE
|
|
298
|
-
══════════════════════════════════════════════════════════════ */
|
|
299
|
-
function generateThemeCss(themeId) {
|
|
300
|
-
var theme = THEMES[themeId];
|
|
301
|
-
if (!theme) return '';
|
|
302
|
-
var css = '/* mdboard theme: ' + theme.name + ' */\n:root {\n';
|
|
303
|
-
for (var key in theme.vars) {
|
|
304
|
-
if (theme.vars.hasOwnProperty(key)) {
|
|
305
|
-
css += ' --' + key + ': ' + theme.vars[key] + ';\n';
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
css += '}\n';
|
|
309
|
-
return css;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function applyTheme(themeId) {
|
|
313
|
-
var theme = THEMES[themeId];
|
|
314
|
-
if (!theme) return;
|
|
315
|
-
var el = document.getElementById('theme-styles');
|
|
316
|
-
if (el) el.textContent = generateThemeCss(themeId);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function revertThemePreview() {
|
|
320
|
-
var el = document.getElementById('theme-styles');
|
|
321
|
-
if (el) el.textContent = '';
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function reloadCustomCss() {
|
|
325
|
-
var link = document.getElementById('custom-theme');
|
|
326
|
-
if (link) link.href = '/mdboard.css?t=' + Date.now();
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function getActiveTheme() {
|
|
330
|
-
return D.config && D.config.theme ? D.config.theme : 'default-dark';
|
|
331
|
-
}
|
package/src/client/editor.js
DELETED
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
/* ══════════════════════════════════════════════════════════════
|
|
2
|
-
EDITOR.JS WRAPPER — md↔blocks conversion + Notion-like UX
|
|
3
|
-
══════════════════════════════════════════════════════════════ */
|
|
4
|
-
|
|
5
|
-
/* ── Inline Markdown ↔ HTML ──────────────────────────────── */
|
|
6
|
-
function inlineMdToHtml(text) {
|
|
7
|
-
if (!text) return '';
|
|
8
|
-
text = text.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
|
|
9
|
-
text = text.replace(/\*(.+?)\*/g, '<i>$1</i>');
|
|
10
|
-
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
11
|
-
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
12
|
-
return text;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function inlineHtmlToMd(html) {
|
|
16
|
-
if (!html) return '';
|
|
17
|
-
var text = html.replace(/<br\s*\/?>/gi, '\n');
|
|
18
|
-
text = text.replace(/<a[^>]+href="([^"]*)"[^>]*>([^<]*)<\/a>/gi, '[$2]($1)');
|
|
19
|
-
text = text.replace(/<b>([^<]*)<\/b>/gi, '**$1**');
|
|
20
|
-
text = text.replace(/<strong>([^<]*)<\/strong>/gi, '**$1**');
|
|
21
|
-
text = text.replace(/<i>([^<]*)<\/i>/gi, '*$1*');
|
|
22
|
-
text = text.replace(/<em>([^<]*)<\/em>/gi, '*$1*');
|
|
23
|
-
text = text.replace(/<code>([^<]*)<\/code>/gi, '`$1`');
|
|
24
|
-
text = text.replace(/<mark[^>]*>([^<]*)<\/mark>/gi, '==$1==');
|
|
25
|
-
text = text.replace(/<[^>]+>/g, '');
|
|
26
|
-
return text;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/* ── Markdown → Editor.js Blocks ────────────────────────── */
|
|
30
|
-
function markdownToBlocks(md) {
|
|
31
|
-
if (!md || !md.trim()) return [{ type: 'paragraph', data: { text: '' } }];
|
|
32
|
-
|
|
33
|
-
var lines = md.split('\n');
|
|
34
|
-
var blocks = [];
|
|
35
|
-
var i = 0;
|
|
36
|
-
|
|
37
|
-
while (i < lines.length) {
|
|
38
|
-
var line = lines[i];
|
|
39
|
-
|
|
40
|
-
if (line.trim() === '') { i++; continue; }
|
|
41
|
-
|
|
42
|
-
if (/^---\s*$/.test(line.trim()) || /^\*\*\*\s*$/.test(line.trim())) {
|
|
43
|
-
blocks.push({ type: 'delimiter', data: {} });
|
|
44
|
-
i++; continue;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
var hMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
48
|
-
if (hMatch) {
|
|
49
|
-
var lvl = Math.min(hMatch[1].length, 6);
|
|
50
|
-
blocks.push({ type: 'header', data: { text: inlineMdToHtml(hMatch[2]), level: lvl } });
|
|
51
|
-
i++; continue;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (line.trim().startsWith('```')) {
|
|
55
|
-
var code = [];
|
|
56
|
-
i++;
|
|
57
|
-
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
58
|
-
code.push(lines[i]);
|
|
59
|
-
i++;
|
|
60
|
-
}
|
|
61
|
-
blocks.push({ type: 'code', data: { code: code.join('\n') } });
|
|
62
|
-
i++; continue;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (line.trim().startsWith('> ')) {
|
|
66
|
-
var quoteLines = [];
|
|
67
|
-
while (i < lines.length && lines[i].trim().startsWith('> ')) {
|
|
68
|
-
quoteLines.push(lines[i].trim().substring(2));
|
|
69
|
-
i++;
|
|
70
|
-
}
|
|
71
|
-
blocks.push({ type: 'quote', data: { text: inlineMdToHtml(quoteLines.join('<br>')), caption: '' } });
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (/^\s*-\s+\[[ x]\]\s+/.test(line)) {
|
|
76
|
-
var checkItems = [];
|
|
77
|
-
while (i < lines.length && /^\s*-\s+\[[ x]\]\s+/.test(lines[i])) {
|
|
78
|
-
var cm = lines[i].match(/^\s*-\s+\[([ x])\]\s+(.+)$/);
|
|
79
|
-
if (cm) checkItems.push({ content: inlineMdToHtml(cm[2]), meta: { checked: cm[1] === 'x' }, items: [] });
|
|
80
|
-
i++;
|
|
81
|
-
}
|
|
82
|
-
blocks.push({ type: 'list', data: { style: 'checklist', items: checkItems } });
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (/^\s*[-*]\s+/.test(line) && !/^\s*-\s+\[/.test(line)) {
|
|
87
|
-
var listItems = [];
|
|
88
|
-
while (i < lines.length && /^\s*[-*]\s+/.test(lines[i]) && !/^\s*-\s+\[/.test(lines[i])) {
|
|
89
|
-
var lm = lines[i].match(/^\s*[-*]\s+(.+)$/);
|
|
90
|
-
if (lm) listItems.push({ content: inlineMdToHtml(lm[1]), items: [] });
|
|
91
|
-
i++;
|
|
92
|
-
}
|
|
93
|
-
blocks.push({ type: 'list', data: { style: 'unordered', items: listItems } });
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (/^\s*\d+\.\s+/.test(line)) {
|
|
98
|
-
var olItems = [];
|
|
99
|
-
while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i])) {
|
|
100
|
-
var om = lines[i].match(/^\s*\d+\.\s+(.+)$/);
|
|
101
|
-
if (om) olItems.push({ content: inlineMdToHtml(om[1]), items: [] });
|
|
102
|
-
i++;
|
|
103
|
-
}
|
|
104
|
-
blocks.push({ type: 'list', data: { style: 'ordered', items: olItems } });
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
var paraLines = [];
|
|
109
|
-
while (i < lines.length && lines[i].trim() !== '' &&
|
|
110
|
-
!/^#{1,6}\s/.test(lines[i]) && !/^```/.test(lines[i]) &&
|
|
111
|
-
!/^>\s/.test(lines[i]) && !/^\s*[-*]\s/.test(lines[i]) &&
|
|
112
|
-
!/^\s*\d+\.\s/.test(lines[i]) && !/^---\s*$/.test(lines[i].trim()) &&
|
|
113
|
-
!/^\*\*\*\s*$/.test(lines[i].trim())) {
|
|
114
|
-
paraLines.push(lines[i]);
|
|
115
|
-
i++;
|
|
116
|
-
}
|
|
117
|
-
if (paraLines.length > 0) {
|
|
118
|
-
blocks.push({ type: 'paragraph', data: { text: inlineMdToHtml(paraLines.join('<br>')) } });
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return blocks.length > 0 ? blocks : [{ type: 'paragraph', data: { text: '' } }];
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/* ── Editor.js Blocks → Markdown ────────────────────────── */
|
|
126
|
-
function blocksToMarkdown(blocks) {
|
|
127
|
-
if (!blocks || blocks.length === 0) return '';
|
|
128
|
-
|
|
129
|
-
var parts = [];
|
|
130
|
-
for (var i = 0; i < blocks.length; i++) {
|
|
131
|
-
var b = blocks[i];
|
|
132
|
-
switch (b.type) {
|
|
133
|
-
case 'header':
|
|
134
|
-
var prefix = '';
|
|
135
|
-
for (var h = 0; h < (b.data.level || 2); h++) prefix += '#';
|
|
136
|
-
parts.push(prefix + ' ' + inlineHtmlToMd(b.data.text));
|
|
137
|
-
break;
|
|
138
|
-
case 'paragraph':
|
|
139
|
-
parts.push(inlineHtmlToMd(b.data.text));
|
|
140
|
-
break;
|
|
141
|
-
case 'list':
|
|
142
|
-
var items = b.data.items || [];
|
|
143
|
-
var listLines = [];
|
|
144
|
-
for (var li = 0; li < items.length; li++) {
|
|
145
|
-
var itemText = typeof items[li] === 'string' ? items[li] : (items[li].content || items[li].text || '');
|
|
146
|
-
if (b.data.style === 'checklist') {
|
|
147
|
-
var chk = (typeof items[li] === 'object' && items[li].meta && items[li].meta.checked) ? 'x' : ' ';
|
|
148
|
-
listLines.push('- [' + chk + '] ' + inlineHtmlToMd(itemText));
|
|
149
|
-
} else if (b.data.style === 'ordered') {
|
|
150
|
-
listLines.push((li + 1) + '. ' + inlineHtmlToMd(itemText));
|
|
151
|
-
} else {
|
|
152
|
-
listLines.push('- ' + inlineHtmlToMd(itemText));
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
parts.push(listLines.join('\n'));
|
|
156
|
-
break;
|
|
157
|
-
case 'checklist':
|
|
158
|
-
var cItems = b.data.items || [];
|
|
159
|
-
var checkLines = [];
|
|
160
|
-
for (var ci = 0; ci < cItems.length; ci++) {
|
|
161
|
-
var check = cItems[ci].checked ? 'x' : ' ';
|
|
162
|
-
checkLines.push('- [' + check + '] ' + inlineHtmlToMd(cItems[ci].text || cItems[ci].content || ''));
|
|
163
|
-
}
|
|
164
|
-
parts.push(checkLines.join('\n'));
|
|
165
|
-
break;
|
|
166
|
-
case 'code':
|
|
167
|
-
parts.push('```\n' + (b.data.code || '') + '\n```');
|
|
168
|
-
break;
|
|
169
|
-
case 'quote':
|
|
170
|
-
var qText = inlineHtmlToMd(b.data.text || '');
|
|
171
|
-
var qLines = qText.split('\n');
|
|
172
|
-
var quoteParts = [];
|
|
173
|
-
for (var qi = 0; qi < qLines.length; qi++) {
|
|
174
|
-
quoteParts.push('> ' + qLines[qi]);
|
|
175
|
-
}
|
|
176
|
-
parts.push(quoteParts.join('\n'));
|
|
177
|
-
break;
|
|
178
|
-
case 'delimiter':
|
|
179
|
-
parts.push('---');
|
|
180
|
-
break;
|
|
181
|
-
default:
|
|
182
|
-
if (b.data && b.data.text) parts.push(inlineHtmlToMd(b.data.text));
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return parts.join('\n\n');
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/* ── Editor.js Instance Management ──────────────────────── */
|
|
189
|
-
function initEditor(holderId, markdown, options) {
|
|
190
|
-
if (typeof EditorJS === 'undefined') {
|
|
191
|
-
var el = document.getElementById(holderId);
|
|
192
|
-
if (el) {
|
|
193
|
-
var ta = document.createElement('textarea');
|
|
194
|
-
ta.value = markdown || '';
|
|
195
|
-
ta.className = 'editor-fallback-textarea';
|
|
196
|
-
ta.placeholder = (options && options.placeholder) || 'Type / for commands...';
|
|
197
|
-
ta.id = holderId + '-fallback';
|
|
198
|
-
el.appendChild(ta);
|
|
199
|
-
}
|
|
200
|
-
return { _fallback: true, _holderId: holderId };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
var opts = options || {};
|
|
204
|
-
var tools = {};
|
|
205
|
-
|
|
206
|
-
if (typeof Header !== 'undefined') {
|
|
207
|
-
tools.header = {
|
|
208
|
-
class: Header,
|
|
209
|
-
inlineToolbar: true,
|
|
210
|
-
config: { levels: [1, 2, 3, 4], defaultLevel: 2 },
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
if (typeof EditorjsList !== 'undefined') tools.list = { class: EditorjsList, inlineToolbar: true, config: { maxLevel: 3 } };
|
|
214
|
-
if (typeof CodeTool !== 'undefined') tools.code = { class: CodeTool };
|
|
215
|
-
if (typeof Quote !== 'undefined') tools.quote = { class: Quote, inlineToolbar: true, config: { quotePlaceholder: 'Write a quote...', captionPlaceholder: '' } };
|
|
216
|
-
if (typeof Delimiter !== 'undefined') tools.delimiter = { class: Delimiter };
|
|
217
|
-
if (typeof Marker !== 'undefined') tools.marker = { class: Marker };
|
|
218
|
-
if (typeof InlineCode !== 'undefined') tools.inlineCode = { class: InlineCode };
|
|
219
|
-
|
|
220
|
-
var blocks = markdownToBlocks(markdown || '');
|
|
221
|
-
|
|
222
|
-
var editor = new EditorJS({
|
|
223
|
-
holder: holderId,
|
|
224
|
-
tools: tools,
|
|
225
|
-
data: { blocks: blocks },
|
|
226
|
-
placeholder: opts.placeholder || 'Type / for commands...',
|
|
227
|
-
minHeight: opts.minHeight || 0,
|
|
228
|
-
autofocus: opts.autofocus || false,
|
|
229
|
-
onChange: opts.onChange || function() {},
|
|
230
|
-
defaultBlock: 'paragraph',
|
|
231
|
-
inlineToolbar: ['bold', 'italic', 'link', 'marker', 'inlineCode'],
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
return editor;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async function getEditorMarkdown(editorInstance) {
|
|
238
|
-
if (!editorInstance) return '';
|
|
239
|
-
if (editorInstance._fallback) {
|
|
240
|
-
var ta = document.getElementById(editorInstance._holderId + '-fallback');
|
|
241
|
-
return ta ? ta.value : '';
|
|
242
|
-
}
|
|
243
|
-
try {
|
|
244
|
-
var data = await editorInstance.save();
|
|
245
|
-
return blocksToMarkdown(data.blocks);
|
|
246
|
-
} catch (e) {
|
|
247
|
-
return '';
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function destroyEditor(editorInstance) {
|
|
252
|
-
if (!editorInstance) return;
|
|
253
|
-
if (editorInstance._fallback) return;
|
|
254
|
-
if (typeof editorInstance.destroy === 'function') {
|
|
255
|
-
try { editorInstance.destroy(); } catch (e) { /* ignore */ }
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/* ── Simple Markdown → HTML Renderer (readonly mode) ──── */
|
|
260
|
-
function simpleMarkdownRender(md) {
|
|
261
|
-
if (!md) return '';
|
|
262
|
-
var lines = md.split('\n');
|
|
263
|
-
var html = '';
|
|
264
|
-
var inCode = false;
|
|
265
|
-
var inList = false;
|
|
266
|
-
var listType = '';
|
|
267
|
-
|
|
268
|
-
for (var i = 0; i < lines.length; i++) {
|
|
269
|
-
var line = lines[i];
|
|
270
|
-
|
|
271
|
-
if (line.trim().startsWith('```')) {
|
|
272
|
-
if (inCode) { html += '</code></pre>'; inCode = false; }
|
|
273
|
-
else { html += '<pre><code>'; inCode = true; }
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
if (inCode) { html += escHtml(line) + '\n'; continue; }
|
|
277
|
-
|
|
278
|
-
if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line)) {
|
|
279
|
-
html += '</' + listType + '>'; inList = false;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (line.trim() === '') { if (!inList) html += '<br>'; continue; }
|
|
283
|
-
if (/^---\s*$/.test(line.trim())) { html += '<hr>'; continue; }
|
|
284
|
-
|
|
285
|
-
var hm = line.match(/^(#{1,6})\s+(.+)$/);
|
|
286
|
-
if (hm) { html += '<h' + hm[1].length + '>' + inlineMdToHtml(hm[2]) + '</h' + hm[1].length + '>'; continue; }
|
|
287
|
-
|
|
288
|
-
if (/^>\s/.test(line)) { html += '<blockquote>' + inlineMdToHtml(line.substring(2)) + '</blockquote>'; continue; }
|
|
289
|
-
|
|
290
|
-
if (/^\s*-\s+\[[ x]\]\s/.test(line)) {
|
|
291
|
-
var chm = line.match(/^\s*-\s+\[([ x])\]\s+(.+)$/);
|
|
292
|
-
if (chm) {
|
|
293
|
-
var checked = chm[1] === 'x';
|
|
294
|
-
html += '<div class="md-check"><span class="md-check-box' + (checked ? ' checked' : '') + '">' + (checked ? '✓' : '') + '</span> ' + inlineMdToHtml(chm[2]) + '</div>';
|
|
295
|
-
}
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (/^\s*[-*]\s+/.test(line)) {
|
|
300
|
-
if (!inList || listType !== 'ul') { if (inList) html += '</' + listType + '>'; html += '<ul>'; inList = true; listType = 'ul'; }
|
|
301
|
-
var lmatch = line.match(/^\s*[-*]\s+(.+)$/);
|
|
302
|
-
if (lmatch) html += '<li>' + inlineMdToHtml(lmatch[1]) + '</li>';
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
if (/^\s*\d+\.\s+/.test(line)) {
|
|
306
|
-
if (!inList || listType !== 'ol') { if (inList) html += '</' + listType + '>'; html += '<ol>'; inList = true; listType = 'ol'; }
|
|
307
|
-
var omatch = line.match(/^\s*\d+\.\s+(.+)$/);
|
|
308
|
-
if (omatch) html += '<li>' + inlineMdToHtml(omatch[1]) + '</li>';
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
html += '<p>' + inlineMdToHtml(line) + '</p>';
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (inCode) html += '</code></pre>';
|
|
316
|
-
if (inList) html += '</' + listType + '>';
|
|
317
|
-
return html;
|
|
318
|
-
}
|