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>
92 lines
3.6 KiB
JavaScript
92 lines
3.6 KiB
JavaScript
// 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 };
|
|
}
|