feat: init
This commit is contained in:
commit
8a609da778
59 files changed
+10053
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user