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:
1 parent
313c41f867
commit
c0587cb267
29 files changed
+5274
-169
No files matched your search
@@ -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 };
|
||||
}
|
||||
Reference in new issue
Block a user