plugin infrastruktur
This commit is contained in:
1 parent
1545b90f39
commit
3f4ba3c0f4
35 files changed
+1303
-195
No files matched your search
+63
-79
@@ -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');
|
||||
|
||||
Reference in new issue
Block a user