| 204 | * byte-for-byte identical to `toggleTaskAtIndex` so round-trip edits stay |
| 205 | * stable. */ |
| 206 | export function parseTasksFromBody(body: string, ctx: ParseTasksContext): VaultTask[] { |
| 207 | const normalized = body.replace(/\r\n/g, '\n') |
| 208 | const { defaults } = parseNoteDefaults(normalized) |
| 209 | const lines = normalized.split('\n') |
| 210 | const tasks: VaultTask[] = [] |
| 211 | |
| 212 | let taskIndex = 0 |
| 213 | let inFence = false |
| 214 | let fenceMarker: string | null = null |
| 215 | |
| 216 | for (let i = 0; i < lines.length; i++) { |
| 217 | const line = lines[i] |
| 218 | |
| 219 | const fenceMatch = line.match(FENCE_RE) |
| 220 | if (fenceMatch) { |
| 221 | const marker = fenceMatch[2] |
| 222 | if (!inFence) { |
| 223 | inFence = true |
| 224 | fenceMarker = marker |
| 225 | } else if (marker === fenceMarker) { |
| 226 | inFence = false |
| 227 | fenceMarker = null |
| 228 | } |
| 229 | continue |
| 230 | } |
| 231 | if (inFence) continue |
| 232 | |
| 233 | const taskMatch = line.match(TASK_LINE_RE) |
| 234 | if (!taskMatch) continue |
| 235 | |
| 236 | const checkedChar = taskMatch[2] |
| 237 | const tail = taskMatch[3].replace(/^\]/, '') // drop the closing `]` of the checkbox |
| 238 | const checked = checkedChar === 'x' || checkedChar === 'X' |
| 239 | |
| 240 | const tokens = extractTokens(tail) |
| 241 | |
| 242 | tasks.push({ |
| 243 | id: `${ctx.path}#${taskIndex}`, |
| 244 | sourcePath: ctx.path, |
| 245 | noteTitle: ctx.title, |
| 246 | noteFolder: ctx.folder, |
| 247 | lineNumber: i, |
| 248 | taskIndex, |
| 249 | rawText: line, |
| 250 | content: tokens.stripped || tail.trim(), |
| 251 | checked, |
| 252 | due: tokens.due ?? defaults.due, |
| 253 | priority: tokens.priority ?? defaults.priority, |
| 254 | waiting: tokens.waiting, |
| 255 | tags: tokens.tags |
| 256 | }) |
| 257 | |
| 258 | taskIndex += 1 |
| 259 | } |
| 260 | |
| 261 | return tasks |
| 262 | } |
| 263 | |