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.
Files changed (53) hide show
  1. package/bin.js +117 -59
  2. package/index.html +2161 -1579
  3. package/package.json +7 -5
  4. package/presets/kanban/api.json +91 -0
  5. package/presets/kanban/cli.json +69 -0
  6. package/presets/kanban/docs.json +29 -0
  7. package/presets/kanban/entities.json +128 -0
  8. package/presets/kanban/structure.json +15 -0
  9. package/presets/kanban/ui.json +86 -0
  10. package/presets/scrum/api.json +98 -0
  11. package/presets/scrum/cli.json +120 -0
  12. package/presets/scrum/docs.json +43 -0
  13. package/presets/scrum/entities.json +268 -0
  14. package/presets/scrum/structure.json +32 -0
  15. package/presets/scrum/ui.json +201 -0
  16. package/presets/shape-up/api.json +40 -0
  17. package/presets/shape-up/cli.json +44 -0
  18. package/presets/shape-up/docs.json +32 -0
  19. package/presets/shape-up/entities.json +140 -0
  20. package/presets/shape-up/structure.json +28 -0
  21. package/presets/shape-up/ui.json +114 -0
  22. package/src/cli/cli.js +186 -210
  23. package/src/cli/config.js +234 -0
  24. package/src/cli/init.js +128 -76
  25. package/src/cli/preset.js +849 -0
  26. package/src/cli/skill.js +417 -0
  27. package/src/cli/status.js +126 -96
  28. package/src/core/config.js +491 -38
  29. package/src/core/history.js +17 -1
  30. package/src/core/scanner.js +373 -463
  31. package/src/core/workspace.js +0 -15
  32. package/src/server/api.js +464 -741
  33. package/src/server/server.js +105 -130
  34. package/build.js +0 -44
  35. package/defaults.json +0 -43
  36. package/src/cli/sync.js +0 -194
  37. package/src/cli/theme.js +0 -142
  38. package/src/client/app.js +0 -266
  39. package/src/client/board.js +0 -157
  40. package/src/client/core.js +0 -331
  41. package/src/client/editor.js +0 -318
  42. package/src/client/history.js +0 -137
  43. package/src/client/metrics.js +0 -38
  44. package/src/client/milestones.js +0 -77
  45. package/src/client/notes.js +0 -183
  46. package/src/client/overview.js +0 -104
  47. package/src/client/panel.js +0 -637
  48. package/src/client/styles.css +0 -471
  49. package/src/client/table.js +0 -111
  50. package/src/client/template.html +0 -144
  51. package/src/client/themes.js +0 -261
  52. package/src/client/workspace.js +0 -164
  53. package/src/core/agent-scanner.js +0 -260
@@ -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
- }
@@ -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 ? '&#10003;' : '') + '</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
- }