plugin infrastruktur

This commit is contained in:
th committed 2026-04-10 07:58:07 +02:00
1 parent 1545b90f39
commit 3f4ba3c0f4
35 files changed
+1303 -195

No files matched your search

+63 -79
View File
@@ -4,7 +4,6 @@ import { $, esc, api, escRe, getCsrf, fuzzyMatch } from './util.js';
import { state, getChildren, CARD_COLORS } from './state.js';
import { renderMd, addCodeCopyButtons, formatTodoItems } from './markdown.js';
import { pushCardEdit, snapshotCard } from './undo.js';
import { openCanvas } from './canvas.js';
const modal = $('modal');
const body = $('editor-body');
@@ -442,87 +441,72 @@ function insertMd(before, after = '', placeholder = '') {
updatePreview();
}
// ── Built-in editor commands ──
// All commands flow through the plugin command registry. Plugins can
// register their own data-md="..." commands via window.hyprcrd.editor.
const _hyprEd = (typeof window !== 'undefined' && window.hyprcrd?.editor) || null;
const _registerCmd = (name, fn) => {
if (_hyprEd) _hyprEd.registerCommand(name, fn);
};
_registerCmd('heading', () => insertMd('## ', '', 'Heading'));
_registerCmd('bold', () => insertMd('**', '**', 'bold'));
_registerCmd('italic', () => insertMd('*', '*', 'italic'));
_registerCmd('strikethrough', () => insertMd('~~', '~~', 'text'));
_registerCmd('code', () => insertMd('`', '`', 'code'));
_registerCmd('codeblock', () => insertMd('```\n', '\n```', 'code'));
_registerCmd('ul', () => insertMd('- ', '', 'item'));
_registerCmd('ol', () => insertMd('1. ', '', 'item'));
_registerCmd('tree', () => insertMd('```tree\n- ', '\n - child\n', 'parent'));
_registerCmd('quote', () => insertMd('> ', '', 'quote'));
_registerCmd('todo', () => {
const s = body.selectionStart, e = body.selectionEnd;
const sel = body.value.substring(s, e);
if (sel && sel.includes('\n') || (sel && /^[\s]*[-*+]\s/m.test(sel))) {
const lines = sel.split('\n');
const hasTodos = lines.some(l => /^\s*- \[[ x]\]/.test(l));
const converted = lines.map(l => {
const trimmed = l.trim();
if (!trimmed) return l;
if (hasTodos) return l.replace(/^(\s*)- \[[ x]\] /, '$1- ');
return l.replace(/^(\s*)(?:[-*+]|\d+\.)\s/, '$1').replace(/^(\s*)(.+)$/, '$1- [ ] $2');
}).join('\n');
body.setRangeText(converted, s, e, 'select');
body.selectionStart = s;
body.selectionEnd = s + converted.length;
updatePreview();
} else {
insertMd('- [ ] ', '', 'task');
}
});
_registerCmd('link', () => insertMd('[', '](url)', 'text'));
_registerCmd('cardlink', () => { insertMd('[[', ']]', ''); showLinkAc(''); });
_registerCmd('extract', async () => {
const sel = body.value.substring(body.selectionStart, body.selectionEnd);
if (!sel.trim()) return;
const title = sel.trim().split('\n')[0].replace(/^#+\s*/, '').replace(/^[-*]\s*/, '').trim();
await window._extractToCard(sel);
const link = `[[${title}]]`;
body.setRangeText(link, body.selectionStart, body.selectionEnd, 'end');
updatePreview();
});
_registerCmd('hr', () => insertMd('\n---\n'));
// Expose editor API to plugins (textarea, insertMd, preview refresh)
if (_hyprEd) {
_hyprEd.api = {
body,
insertMd,
updatePreview,
};
}
// Toolbar dispatch routes through the plugin registry
$('md-toolbar')?.addEventListener('click', e => {
const btn = e.target.closest('[data-md]');
if (!btn) return;
const cmds = {
heading: () => insertMd('## ', '', 'Heading'), bold: () => insertMd('**', '**', 'bold'),
italic: () => insertMd('*', '*', 'italic'), strikethrough: () => insertMd('~~', '~~', 'text'),
code: () => insertMd('`', '`', 'code'), codeblock: () => insertMd('```\n', '\n```', 'code'),
ul: () => insertMd('- ', '', 'item'), ol: () => insertMd('1. ', '', 'item'),
tree: () => insertMd('```tree\n- ', '\n - child\n', 'parent'),
quote: () => insertMd('> ', '', 'quote'),
todo: () => {
const s = body.selectionStart, e = body.selectionEnd;
const sel = body.value.substring(s, e);
if (sel && sel.includes('\n') || (sel && /^[\s]*[-*+]\s/m.test(sel))) {
// Multi-line or bullet list: convert each line
const lines = sel.split('\n');
const hasTodos = lines.some(l => /^\s*- \[[ x]\]/.test(l));
const converted = lines.map(l => {
const trimmed = l.trim();
if (!trimmed) return l;
if (hasTodos) {
// Toggle back: remove checkbox
return l.replace(/^(\s*)- \[[ x]\] /, '$1- ');
}
// Add checkbox: strip existing bullet if present
return l.replace(/^(\s*)(?:[-*+]|\d+\.)\s/, '$1').replace(/^(\s*)(.+)$/, '$1- [ ] $2');
}).join('\n');
body.setRangeText(converted, s, e, 'select');
body.selectionStart = s;
body.selectionEnd = s + converted.length;
updatePreview();
} else {
insertMd('- [ ] ', '', 'task');
}
},
link: () => insertMd('[', '](url)', 'text'),
cardlink: () => { insertMd('[[', ']]', ''); showLinkAc(''); },
extract: async () => {
const sel = body.value.substring(body.selectionStart, body.selectionEnd);
if (!sel.trim()) return;
const title = sel.trim().split('\n')[0].replace(/^#+\s*/, '').replace(/^[-*]\s*/, '').trim();
await window._extractToCard(sel);
// Replace selection with a card link
const link = `[[${title}]]`;
body.setRangeText(link, body.selectionStart, body.selectionEnd, 'end');
updatePreview();
},
hr: () => insertMd('\n---\n'),
asciiart: () => {
// Check if cursor is inside an existing ```ascii block to edit it
const val = body.value;
const pos = body.selectionStart;
const before = val.substring(0, pos);
const after = val.substring(pos);
const blockStart = before.lastIndexOf('```ascii\n');
const blockEndInBefore = blockStart >= 0 ? before.indexOf('\n```', blockStart + 9) : -1;
let existingText = null, replaceStart = 0, replaceEnd = 0;
if (blockStart >= 0 && blockEndInBefore < 0) {
// Cursor is inside a ```ascii block
const contentStart = blockStart + 9; // after "```ascii\n"
const endMark = val.indexOf('\n```', contentStart);
if (endMark >= 0) {
existingText = val.substring(contentStart, endMark);
replaceStart = blockStart;
replaceEnd = endMark + 4;
}
}
openCanvas(text => {
const block = '```ascii\n' + text + '\n```';
if (existingText !== null) {
body.setRangeText(block, replaceStart, replaceEnd, 'end');
} else {
const s = body.selectionStart;
const prefix = s > 0 && val[s - 1] !== '\n' ? '\n' : '';
body.setRangeText(prefix + block + '\n', s, body.selectionEnd, 'end');
}
updatePreview();
}, existingText);
},
};
cmds[btn.dataset.md]?.();
const fn = _hyprEd?.getCommand(btn.dataset.md);
if (fn) fn();
});
const editorDivider = $('editor-divider');