memos/web/src/utils/markdown-manipulation.ts
Claude 739fd2cde6 refactor: update markdown parser
- Removed the `nodes` field from the `Memo` interface in `memo_service.ts`.
- Updated the `createBaseMemo` function and the `Memo` message functions to reflect the removal of `nodes`.
- Cleaned up the serialization and deserialization logic accordingly.

chore: remove code-inspector-plugin from Vite configuration

- Deleted the `codeInspectorPlugin` from the Vite configuration in `vite.config.mts`.
- Simplified the plugins array to include only `react` and `tailwindcss`.
2025-10-26 11:28:40 +08:00

211 lines
5.2 KiB
TypeScript

/**
* Utilities for manipulating markdown strings (GitHub-style approach)
*
* These functions modify the raw markdown text directly without parsing to AST.
* This is the same approach GitHub uses for task list updates.
*/
/**
* Toggle a task checkbox at a specific line number
*
* @param markdown - The full markdown content
* @param lineNumber - Zero-based line number
* @param checked - New checked state
* @returns Updated markdown string
*/
export function toggleTaskAtLine(markdown: string, lineNumber: number, checked: boolean): string {
const lines = markdown.split("\n");
if (lineNumber < 0 || lineNumber >= lines.length) {
return markdown;
}
const line = lines[lineNumber];
// Match task list patterns: - [ ], - [x], - [X], etc.
const taskPattern = /^(\s*[-*+]\s+)\[([ xX])\](\s+.*)$/;
const match = line.match(taskPattern);
if (!match) {
// Not a task list item
return markdown;
}
const [, prefix, , suffix] = match;
const newCheckmark = checked ? "x" : " ";
lines[lineNumber] = `${prefix}[${newCheckmark}]${suffix}`;
return lines.join("\n");
}
/**
* Toggle a task checkbox by its index (nth task in the document)
*
* @param markdown - The full markdown content
* @param taskIndex - Zero-based index of the task (0 = first task, 1 = second task, etc.)
* @param checked - New checked state
* @returns Updated markdown string
*/
export function toggleTaskAtIndex(markdown: string, taskIndex: number, checked: boolean): string {
const lines = markdown.split("\n");
const taskPattern = /^(\s*[-*+]\s+)\[([ xX])\](\s+.*)$/;
let currentTaskIndex = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const match = line.match(taskPattern);
if (match) {
if (currentTaskIndex === taskIndex) {
const [, prefix, , suffix] = match;
const newCheckmark = checked ? "x" : " ";
lines[i] = `${prefix}[${newCheckmark}]${suffix}`;
break;
}
currentTaskIndex++;
}
}
return lines.join("\n");
}
/**
* Remove all completed tasks from markdown
*
* @param markdown - The full markdown content
* @returns Markdown with completed tasks removed
*/
export function removeCompletedTasks(markdown: string): string {
const lines = markdown.split("\n");
const completedTaskPattern = /^(\s*[-*+]\s+)\[([xX])\](\s+.*)$/;
const result: string[] = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Skip completed tasks
if (completedTaskPattern.test(line)) {
// Also skip the following line if it's empty (preserve spacing)
if (i + 1 < lines.length && lines[i + 1].trim() === "") {
i++; // Skip next line
}
continue;
}
result.push(line);
}
return result.join("\n");
}
/**
* Count tasks in markdown
*
* @param markdown - The full markdown content
* @returns Object with task counts
*/
export function countTasks(markdown: string): {
total: number;
completed: number;
incomplete: number;
} {
const lines = markdown.split("\n");
const taskPattern = /^(\s*[-*+]\s+)\[([ xX])\](\s+.*)$/;
let total = 0;
let completed = 0;
for (const line of lines) {
const match = line.match(taskPattern);
if (match) {
total++;
const checkmark = match[2];
if (checkmark.toLowerCase() === "x") {
completed++;
}
}
}
return {
total,
completed,
incomplete: total - completed,
};
}
/**
* Check if markdown has any completed tasks
*
* @param markdown - The full markdown content
* @returns True if there are completed tasks
*/
export function hasCompletedTasks(markdown: string): boolean {
const completedTaskPattern = /^(\s*[-*+]\s+)\[([xX])\](\s+.*)$/m;
return completedTaskPattern.test(markdown);
}
/**
* Get the line number of the nth task
*
* @param markdown - The full markdown content
* @param taskIndex - Zero-based task index
* @returns Line number, or -1 if not found
*/
export function getTaskLineNumber(markdown: string, taskIndex: number): number {
const lines = markdown.split("\n");
const taskPattern = /^(\s*[-*+]\s+)\[([ xX])\](\s+.*)$/;
let currentTaskIndex = 0;
for (let i = 0; i < lines.length; i++) {
if (taskPattern.test(lines[i])) {
if (currentTaskIndex === taskIndex) {
return i;
}
currentTaskIndex++;
}
}
return -1;
}
/**
* Extract all task items with their metadata
*
* @param markdown - The full markdown content
* @returns Array of task metadata
*/
export interface TaskItem {
lineNumber: number;
taskIndex: number;
checked: boolean;
content: string;
indentation: number;
}
export function extractTasks(markdown: string): TaskItem[] {
const lines = markdown.split("\n");
const taskPattern = /^(\s*)([-*+]\s+)\[([ xX])\](\s+.*)$/;
const tasks: TaskItem[] = [];
let taskIndex = 0;
for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
const line = lines[lineNumber];
const match = line.match(taskPattern);
if (match) {
const [, indentStr, , checkmark, content] = match;
tasks.push({
lineNumber,
taskIndex: taskIndex++,
checked: checkmark.toLowerCase() === "x",
content: content.trim(),
indentation: indentStr.length,
});
}
}
return tasks;
}