feat(app): editor engine, markdown, store and domain modules

Block editor, markdown round trip, formulas/databases/kanban/canvas
models, graph simulation (time-budgeted ticks for big vaults), shortcut
registry, dialog focus handling, HTML to Markdown for paste and clipping.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Your NameandClaude Sonnet 5.5 committed 2026-10-15 11:57:09 +02:00
1 parent 313c41f867
commit c0587cb267
29 files changed
+5274 -169

No files matched your search

+91
View File
@@ -0,0 +1,91 @@
// Kanban boards stored as plain Markdown, in the same shape as the Obsidian Kanban plugin:
//
// ---
// kanban-plugin: basic <- front matter marks the page as a board
// ---
//
// ## To do <- a column
//
// - [ ] Write the intro <- a card (text keeps inline Markdown: **bold**, [[links]] …)
// with a second line <- indented lines belong to the card above
// - [x] Pick a title <- a checked card
//
// %% kanban:settings ... %% <- anything from a *** or %% line on is kept as is
//
// Lines in a column that aren't cards (e.g. the plugin's **Complete** marker) are kept too.
import { parseMarkdown, htmlToMd } from './markdown.js';
export const BOARD_KEY = 'kanban-plugin';
export const isBoard = (meta) => !!(meta && meta[BOARD_KEY]);
let seq = 0;
const uid = (p) => p + (++seq).toString(36) + Math.random().toString(36).slice(2, 6);
export const card = (text = '', done = false) => ({ id: uid('k'), text, done });
export const column = (title = 'Untitled', cards = []) => ({ id: uid('c'), title, extra: [], cards });
export function parseBoard(src) {
const lines = String(src || '').replace(/\r\n?/g, '\n').split('\n');
const board = { head: '', columns: [], tail: '' };
const head = [];
let col = null;
let last = null; // card that indented lines continue
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const t = line.trim();
if (t === '***' || t.startsWith('%% kanban')) {
board.tail = lines.slice(i).join('\n').replace(/\s+$/, '');
break;
}
let m;
if ((m = line.match(/^##\s+(.*?)\s*$/))) {
col = column(m[1]);
board.columns.push(col);
last = null;
continue;
}
if (!col) { head.push(line); continue; }
if ((m = line.match(/^[-*+]\s+(?:\[([ xX])\]\s?)?(.*)$/))) {
last = card(m[2], !!m[1] && m[1] !== ' ');
col.cards.push(last);
continue;
}
if (last && /^(\s{2,}|\t)\S/.test(line)) { last.text += '\n' + line.replace(/^(\s{2}|\t)/, ''); continue; }
if (line.trim()) { col.extra.push(line); last = null; }
}
board.head = head.join('\n').trim();
return board;
}
export function boardToMd(board) {
const parts = [];
if (board.head) parts.push(board.head);
for (const c of board.columns) {
const body = [...c.extra, ...c.cards.map((k) => `- [${k.done ? 'x' : ' '}] ` + k.text.replace(/\n/g, '\n '))];
parts.push(`## ${c.title}` + (body.length ? '\n\n' + body.join('\n') : ''));
}
if (board.tail) parts.push(board.tail);
return parts.length ? parts.join('\n\n\n') + '\n' : '';
}
export const defaultBoard = () => ({ head: '', tail: '', columns: [column('To do'), column('Doing'), column('Done')] });
// Turn an ordinary page into a board: each ## heading becomes a column, list items and paragraphs become cards.
// Anything before the first heading lands in a "To do" column.
export function boardFromMarkdown(body) {
const blocks = parseMarkdown(body);
const columns = [];
let col = null;
for (const b of blocks) {
if (b.type === 'h1' || b.type === 'h2' || b.type === 'h3') {
col = column(htmlToMd(b.html).trim() || 'Untitled');
columns.push(col);
continue;
}
const text = b.type === 'code' ? '`' + (b.text || '').split('\n')[0] + '`' : b.type === 'toggle' ? htmlToMd(b.html) : htmlToMd(b.html);
if (b.type === 'divider' || b.type === 'image' || !text.trim()) continue;
if (!col) { col = column('To do'); columns.unshift(col); }
col.cards.push(card(text.trim().replace(/\n+/g, '\n'), !!b.checked));
}
if (!columns.length) return defaultBoard();
return { head: '', tail: '', columns };
}