feat: init

This commit is contained in:
hh committed 2026-08-08 16:30:22 +02:00
commit 8a609da778
59 files changed
+10053

No files matched your search

+146
View File
@@ -0,0 +1,146 @@
import { findLane, laneInsertPoint, parseBoard } from "../model/board";
import type { Task } from "../model/types";
export class StaleTaskError extends Error {
constructor(public readonly task: Task) {
super(`line no longer present in ${task.path}`);
this.name = "StaleTaskError";
}
}
export class MissingLaneError extends Error {
constructor(
public readonly path: string,
public readonly project: string,
) {
super(`no lane "${project}" in ${path}`);
this.name = "MissingLaneError";
}
}
export function locate(lines: string[], task: Task): number {
if (lines[task.line] === task.raw) return task.line;
let best = -1;
let bestDistance = Number.POSITIVE_INFINITY;
for (let i = 0; i < lines.length; i += 1) {
if (lines[i] !== task.raw) continue;
const distance = Math.abs(i - task.line);
if (distance < bestDistance) {
best = i;
bestDistance = distance;
}
}
if (best === -1) throw new StaleTaskError(task);
return best;
}
export function extent(lines: string[], at: number, task: Task): number {
let count = 1;
while (count <= task.body.length && lines[at + count] === task.body[count - 1]) {
count += 1;
}
return count;
}
export function replaceLine(lines: string[], task: Task, text: string): void {
const at = locate(lines, task);
lines.splice(at, 1, ...text.split("\n"));
}
export function cutTask(lines: string[], task: Task): string[] {
const at = locate(lines, task);
const block = lines.splice(at, extent(lines, at, task));
while (lines[at] === "" && lines[at - 1] === "" && lines[at + 1] !== undefined) {
lines.splice(at, 1);
}
return block;
}
export interface InsertTarget {
project: string;
index?: number;
}
export function insertBlock(
lines: string[],
path: string,
target: InsertTarget,
block: string[],
): void {
const board = parseBoard(path, lines.join("\n"));
const lane = findLane(board, target.project);
if (!lane) throw new MissingLaneError(path, target.project);
const siblings = board.tasks.filter(
(task) => task.project === target.project && !task.inArchive,
);
const index = target.index ?? siblings.length;
const at =
index < siblings.length ? siblings[index].line : laneInsertPoint(board, lane);
lines.splice(at, 0, ...block);
}
export function applyReorder(
lines: string[],
inLane: Task[],
ordered: Task[],
): void {
if (inLane.length < 2 || ordered.length !== inLane.length) return;
const slots = inLane.map((task) => {
const at = locate(lines, task);
return { at, size: extent(lines, at, task) };
});
const blocks = ordered.map((task) => {
const at = locate(lines, task);
return lines.slice(at, at + extent(lines, at, task));
});
for (let i = slots.length - 1; i >= 0; i -= 1) {
lines.splice(slots[i].at, slots[i].size);
}
for (let i = 0; i < slots.length; i += 1) {
lines.splice(slots[i].at, 0, ...blocks[i]);
}
}
const SETTINGS_OPEN = /^%%\s*kanban:settings\s*$/u;
export function applyArchive(
lines: string[],
path: string,
archiveHeading: string,
): number {
const board = parseBoard(path, lines.join("\n"));
const done = board.tasks.filter(
(task) => !task.inArchive && (task.status === "x" || task.status === "-"),
);
if (done.length === 0) return 0;
const blocks: string[][] = [];
for (const task of [...done].reverse()) {
const at = locate(lines, task);
blocks.unshift(lines.splice(at, extent(lines, at, task)));
}
const heading = `## ${archiveHeading}`;
let archiveAt = lines.findIndex((line) => line.trim() === heading);
if (archiveAt === -1) {
const settingsAt = lines.findIndex((line) => SETTINGS_OPEN.test(line));
let tail = settingsAt === -1 ? lines.length : settingsAt;
while (tail > 0 && lines[tail - 1].trim() === "") tail -= 1;
lines.splice(tail, 0, "", "***", "", heading, "");
archiveAt = tail + 3;
}
let insertAt = archiveAt + 1;
while (
insertAt < lines.length &&
!/^##\s/u.test(lines[insertAt]) &&
!SETTINGS_OPEN.test(lines[insertAt])
) {
insertAt += 1;
}
while (insertAt > archiveAt + 1 && lines[insertAt - 1].trim() === "") {
insertAt -= 1;
}
lines.splice(insertAt, 0, ...blocks.flat());
return blocks.length;
}