imdone-core 2.1.6 → 2.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,161 @@
1
1
  import { Eta } from "eta"
2
2
  import uniqid from 'uniqid';
3
+ import { logger } from '../logger.js';
3
4
 
4
5
  function removeDisplayComments(description) {
5
- return description.replace(/<!--\s*\[([\s\S]*?)\]\s*-->/g, '$1');
6
+ const startTime = Date.now();
7
+ logger.debug(`[INSTRUMENT] removeDisplayComments START - contentLength: ${description?.length || 0}`);
8
+
9
+ // State machine implementation to avoid catastrophic backtracking
10
+ // Looking for pattern: <!-- [ content ] -->
11
+ let result = '';
12
+ const len = description.length;
13
+ let i = 0;
14
+ let iterations = 0;
15
+
16
+ while (i < len) {
17
+ iterations++;
18
+ if (iterations % 10000 === 0) {
19
+ logger.debug(`[INSTRUMENT] removeDisplayComments - iteration ${iterations}, position ${i}/${len}`);
20
+ }
21
+ // Check for HTML comment start: <!--
22
+ if (i <= len - 4 &&
23
+ description[i] === '<' &&
24
+ description[i + 1] === '!' &&
25
+ description[i + 2] === '-' &&
26
+ description[i + 3] === '-') {
27
+
28
+ const commentStart = i;
29
+ i += 4; // Skip <!--
30
+
31
+ // Skip whitespace
32
+ while (i < len && /\s/.test(description[i])) {
33
+ i++;
34
+ }
35
+
36
+ // Check for opening bracket
37
+ if (i < len && description[i] === '[') {
38
+ i++; // Skip [
39
+ const contentStart = i;
40
+
41
+ // Find closing bracket followed by optional whitespace and -->
42
+ while (i < len) {
43
+ if (description[i] === ']') {
44
+ const contentEnd = i;
45
+ i++; // Skip ]
46
+
47
+ // Skip whitespace
48
+ while (i < len && /\s/.test(description[i])) {
49
+ i++;
50
+ }
51
+
52
+ // Check for comment end: -->
53
+ if (i <= len - 3 &&
54
+ description[i] === '-' &&
55
+ description[i + 1] === '-' &&
56
+ description[i + 2] === '>') {
57
+ // Found complete pattern - extract content
58
+ result += description.substring(contentStart, contentEnd);
59
+ i += 3; // Skip -->
60
+ break;
61
+ }
62
+ }
63
+ i++;
64
+ }
65
+
66
+ // If we didn't find the complete pattern, include what we had
67
+ if (i >= len) {
68
+ result += description.substring(commentStart, i);
69
+ }
70
+ continue;
71
+ }
72
+
73
+ // No opening bracket after <!--, include the comment start and continue
74
+ result += description.substring(commentStart, i);
75
+ continue;
76
+ }
77
+
78
+ result += description[i];
79
+ i++;
80
+ }
81
+
82
+ const elapsed = Date.now() - startTime;
83
+ logger.debug(`[INSTRUMENT] removeDisplayComments END - elapsed: ${elapsed}ms, iterations: ${iterations}`);
84
+ return result;
6
85
  }
7
86
 
8
- function escapeCodeBlocks(content) {
87
+ function escapeCodeBlocks(content, useRegex = false) {
88
+ if (useRegex) {
89
+ // Old regex-based implementation (can cause catastrophic backtracking)
90
+ // Kept for testing/comparison purposes only
91
+ return _escapeCodeBlocksRegex(content);
92
+ }
93
+
94
+ // New state machine implementation (safe from catastrophic backtracking)
95
+ let codeBlocks = [];
96
+ let result = '';
97
+ const len = content.length;
98
+ let i = 0;
99
+
100
+ while (i < len) {
101
+ // Check for code block (```)
102
+ if (content[i] === '`' && content[i + 1] === '`' && content[i + 2] === '`') {
103
+ const start = i;
104
+ i += 3; // Skip opening ```
105
+
106
+ // Find closing ```
107
+ while (i < len - 2) {
108
+ if (content[i] === '`' && content[i + 1] === '`' && content[i + 2] === '`') {
109
+ i += 3; // Include closing ```
110
+ const match = content.substring(start, i);
111
+ const key = `__CODE_BLOCK_${codeBlocks.length}__`;
112
+ codeBlocks.push(match);
113
+ result += key;
114
+ break;
115
+ }
116
+ i++;
117
+ }
118
+ // If we didn't find a closing ```, include what we have
119
+ if (i >= len - 2 && content[i - 3] !== '`') {
120
+ result += content.substring(start, i);
121
+ }
122
+ continue;
123
+ }
124
+
125
+ // Check for inline code (`)
126
+ if (content[i] === '`') {
127
+ const start = i;
128
+ i++; // Skip opening `
129
+
130
+ // Find closing `
131
+ while (i < len) {
132
+ if (content[i] === '`') {
133
+ i++; // Include closing `
134
+ const match = content.substring(start, i);
135
+ const key = `__CODE_BLOCK_${codeBlocks.length}__`;
136
+ codeBlocks.push(match);
137
+ result += key;
138
+ break;
139
+ }
140
+ i++;
141
+ }
142
+ // If we didn't find a closing `, include what we have
143
+ if (i >= len && content[i - 1] !== '`') {
144
+ result += content.substring(start, i);
145
+ }
146
+ continue;
147
+ }
148
+
149
+ result += content[i];
150
+ i++;
151
+ }
152
+
153
+ return { content: result, codeBlocks };
154
+ }
155
+
156
+ function _escapeCodeBlocksRegex(content) {
157
+ // OLD IMPLEMENTATION - DO NOT USE IN PRODUCTION
158
+ // This regex can cause catastrophic backtracking with nested backticks
9
159
  const codeBlockRegex = /(```[\s\S]*?```|`[^`]*`)/g;
10
160
  let codeBlocks = [];
11
161
 
@@ -28,41 +178,105 @@ function restoreCodeBlocks(content, codeBlocks) {
28
178
  }
29
179
 
30
180
  /**
31
- * Interpolates the content with the provided data.
181
+ * Interpolates the content with the provided data using state machine parsing.
32
182
  * @param {string} content - The content to interpolate.
33
183
  * @param {object} data - The data to use for interpolation.
34
184
  * @param {object} opts - The options for interpolation.
35
185
  * @returns {object} - The interpolated content, whether there was interpolation, and error tokens.
36
186
  */
37
187
  export function interpolate(content, data, opts = { tags: ['${', '}'] }) {
188
+ const startTime = Date.now();
189
+ logger.debug(`[INSTRUMENT] interpolate START - contentLength: ${content?.length || 0}, tags: ${JSON.stringify(opts.tags)}`);
190
+
38
191
  const errorTokens = {};
39
192
  const { tags } = opts;
40
- // /(?<![`\\])\${([\s\S]+?)}/g
41
- const regex = new RegExp(`(?<![\`\\\\])\\${tags[0]}([\\s\\S]+?)${tags[1]}`, 'g');
193
+ const [startTag, endTag] = tags;
42
194
  let hasInterpolation = false;
43
195
 
44
- content = content.replace(regex, (match, p1) => {
45
- try {
46
- const eta = new Eta({ tags, useWith: true, autoEscape: false });
47
- const [startTag, endTag] = opts.tags;
48
- const template = `${startTag}= ${p1} ${endTag}`;
49
- const result = eta.renderString(template, data);
50
- if (result !== match) {
51
- hasInterpolation = true;
196
+ // State machine implementation to avoid catastrophic backtracking
197
+ let result = '';
198
+ const len = content.length;
199
+ let i = 0;
200
+ let iterations = 0;
201
+
202
+ while (i < len) {
203
+ iterations++;
204
+ if (iterations % 10000 === 0) {
205
+ logger.debug(`[INSTRUMENT] interpolate - iteration ${iterations}, position ${i}/${len}, elapsed: ${Date.now() - startTime}ms`);
206
+ }
207
+
208
+ // Check if we're at a potential start tag
209
+ // Skip if preceded by backtick or backslash
210
+ const prevChar = i > 0 ? content[i - 1] : '';
211
+ const isEscaped = prevChar === '`' || prevChar === '\\';
212
+
213
+ // Check for start tag match
214
+ let startTagMatches = !isEscaped && true;
215
+ for (let j = 0; j < startTag.length && startTagMatches; j++) {
216
+ if (i + j >= len || content[i + j] !== startTag[j]) {
217
+ startTagMatches = false;
52
218
  }
53
- return result || '';
54
- } catch (e) {
55
- const token = `__ERROR_TOKEN_${uniqid()}__`;
56
- errorTokens[token] = match;
57
- return token || '';
58
219
  }
59
- });
60
220
 
61
- return { content, hasInterpolation, errorTokens };
221
+ if (startTagMatches) {
222
+ // Found start tag - look for matching end tag
223
+ const matchStart = i;
224
+ i += startTag.length;
225
+ const contentStart = i;
226
+
227
+ // Find the end tag
228
+ let foundEnd = false;
229
+ while (i < len) {
230
+ let endTagMatches = true;
231
+ for (let j = 0; j < endTag.length && endTagMatches; j++) {
232
+ if (i + j >= len || content[i + j] !== endTag[j]) {
233
+ endTagMatches = false;
234
+ }
235
+ }
236
+
237
+ if (endTagMatches) {
238
+ // Found end tag
239
+ const templateContent = content.substring(contentStart, i);
240
+ i += endTag.length;
241
+ foundEnd = true;
242
+
243
+ // Try to interpolate
244
+ try {
245
+ const eta = new Eta({ tags, useWith: true, autoEscape: false });
246
+ const template = `${startTag}= ${templateContent} ${endTag}`;
247
+ const interpolated = eta.renderString(template, data);
248
+ result += interpolated || '';
249
+ hasInterpolation = true;
250
+ } catch (e) {
251
+ // On error, preserve the original match with an error token
252
+ const token = `__ERROR_TOKEN_${uniqid()}__`;
253
+ const originalMatch = content.substring(matchStart, i);
254
+ errorTokens[token] = originalMatch;
255
+ result += token;
256
+ }
257
+ break;
258
+ }
259
+ i++;
260
+ }
261
+
262
+ // If no end tag found, include the start tag and continue
263
+ if (!foundEnd) {
264
+ result += content.substring(matchStart, i);
265
+ }
266
+ continue;
267
+ }
268
+
269
+ result += content[i];
270
+ i++;
271
+ }
272
+
273
+ const elapsed = Date.now() - startTime;
274
+ logger.debug(`[INSTRUMENT] interpolate END - elapsed: ${elapsed}ms, iterations: ${iterations}, hasInterpolation: ${hasInterpolation}`);
275
+ return { content: result, hasInterpolation, errorTokens };
62
276
  }
63
277
 
64
278
  export function encodeMarkdownLinks(content) {
65
- return content.replace(/(?<!!)\[(.*?)\]\((.*?)( ".*?")?\)/g, (match, p1, p2, p3) => {
279
+ return content.replace(/(?<!!)\[(.*?)\]\((.*?)( ".*?")?\)/g, (_match, p1, p2, p3) => {
66
280
  // URI encode file links
67
281
  const title = p3 || '';
68
282
  const encodedUrl = p2.replace(/\s/g, '%20').replace(/\)/g, '%29').replace(/\(/g, '%28');
@@ -78,17 +292,29 @@ export function encodeMarkdownLinks(content) {
78
292
  }
79
293
 
80
294
  export function format(content, data, mustache) {
295
+ const formatStart = Date.now();
296
+ logger.debug(`[INSTRUMENT] format START - contentLength: ${content?.length || 0}, mustache: ${mustache}`);
297
+
81
298
  const errorTokens = {};
82
299
 
83
- if (mustache) content = removeDisplayComments(content);
300
+ if (mustache) {
301
+ logger.debug(`[INSTRUMENT] format - calling removeDisplayComments`);
302
+ content = removeDisplayComments(content);
303
+ }
84
304
 
85
305
  // Escape code blocks before processing interpolation
306
+ logger.debug(`[INSTRUMENT] format - calling escapeCodeBlocks`);
307
+ const escapeStart = Date.now();
86
308
  const { content: escapedContent, codeBlocks } = escapeCodeBlocks(content);
309
+ logger.debug(`[INSTRUMENT] format - escapeCodeBlocks completed in ${Date.now() - escapeStart}ms`);
87
310
 
88
311
  let result = escapedContent;
89
312
  let hasInterpolation = true;
313
+ let interpolationRound = 0;
90
314
 
91
315
  while (hasInterpolation) {
316
+ interpolationRound++;
317
+ logger.debug(`[INSTRUMENT] format - interpolation round ${interpolationRound} with tags ['${'${'}', '}']`);
92
318
  const interpolationResult = interpolate(result, data, { tags: ['${', '}'] });
93
319
  result = interpolationResult.content;
94
320
  hasInterpolation = interpolationResult.hasInterpolation;
@@ -97,7 +323,10 @@ export function format(content, data, mustache) {
97
323
 
98
324
  if (mustache) {
99
325
  hasInterpolation = true;
326
+ interpolationRound = 0;
100
327
  while (hasInterpolation) {
328
+ interpolationRound++;
329
+ logger.debug(`[INSTRUMENT] format - mustache interpolation round ${interpolationRound} with tags ['{{', '}}']`);
101
330
  const interpolationResult = interpolate(result, data, { tags: ['{{', '}}'] });
102
331
  result = interpolationResult.content;
103
332
  hasInterpolation = interpolationResult.hasInterpolation;
@@ -105,6 +334,7 @@ export function format(content, data, mustache) {
105
334
  }
106
335
  }
107
336
 
337
+ logger.debug(`[INSTRUMENT] format - calling restoreCodeBlocks`);
108
338
  result = restoreCodeBlocks(result, codeBlocks);
109
339
 
110
340
  // Restore error tokens
@@ -112,5 +342,7 @@ export function format(content, data, mustache) {
112
342
  result = result.replace(new RegExp(token, 'g'), errorTokens[token]);
113
343
  });
114
344
 
345
+ const elapsed = Date.now() - formatStart;
346
+ logger.debug(`[INSTRUMENT] format END - elapsed: ${elapsed}ms`);
115
347
  return result;
116
348
  }
package/lib/file.js CHANGED
@@ -1276,8 +1276,10 @@ export class File extends Emitter {
1276
1276
  if (lang.block) {
1277
1277
  var start = escapeRegExp(lang.block.start)
1278
1278
  var end = escapeRegExp(lang.block.end)
1279
- //
1280
- reString = reString + '|(?<!["\'`])' + start + '(.|[\\r\\n])*?' + end
1279
+ // Use [^] instead of (.|[\r\n]) and remove lazy quantifier for better performance
1280
+ // [^] matches any character including newlines without the overhead of alternation
1281
+ // Using greedy match with proper end delimiter is safer than lazy quantifier
1282
+ reString = reString + '|(?<!["\'`])' + start + '[^]*?' + end
1281
1283
  }
1282
1284
 
1283
1285
  return new RegExp(reString, 'gmi')
@@ -1428,7 +1430,62 @@ export class File extends Emitter {
1428
1430
  return Task.isResultInMarkdownCode(positions, pos)
1429
1431
  }
1430
1432
 
1431
- getMarkdownCodePositions (text) {
1433
+ getMarkdownCodePositions (text, useRegex = false) {
1434
+ if (useRegex) {
1435
+ // Old regex-based implementation (can cause catastrophic backtracking)
1436
+ // Kept for testing/comparison purposes only
1437
+ return this._getMarkdownCodePositionsRegex(text);
1438
+ }
1439
+
1440
+ // New state machine implementation (safe from catastrophic backtracking)
1441
+ const positions = [];
1442
+ const len = text.length;
1443
+ let i = 0;
1444
+
1445
+ while (i < len) {
1446
+ // Check for code block (```)
1447
+ if (text[i] === '`' && text[i + 1] === '`' && text[i + 2] === '`') {
1448
+ const start = i;
1449
+ i += 3; // Skip opening ```
1450
+
1451
+ // Find closing ```
1452
+ while (i < len - 2) {
1453
+ if (text[i] === '`' && text[i + 1] === '`' && text[i + 2] === '`') {
1454
+ i += 3; // Include closing ```
1455
+ positions.push({ start, end: i });
1456
+ break;
1457
+ }
1458
+ i++;
1459
+ }
1460
+ continue;
1461
+ }
1462
+
1463
+ // Check for inline code (`)
1464
+ if (text[i] === '`') {
1465
+ const start = i;
1466
+ i++; // Skip opening `
1467
+
1468
+ // Find closing `
1469
+ while (i < len) {
1470
+ if (text[i] === '`') {
1471
+ i++; // Include closing `
1472
+ positions.push({ start, end: i });
1473
+ break;
1474
+ }
1475
+ i++;
1476
+ }
1477
+ continue;
1478
+ }
1479
+
1480
+ i++;
1481
+ }
1482
+
1483
+ return positions;
1484
+ };
1485
+
1486
+ _getMarkdownCodePositionsRegex (text) {
1487
+ // OLD IMPLEMENTATION - DO NOT USE IN PRODUCTION
1488
+ // This regex can cause catastrophic backtracking with nested backticks
1432
1489
  const positions = [];
1433
1490
  const codeBlockRe = /```[\s\S]*?```/g;
1434
1491
  const inlineCodeRe = /`[^`]*`/g;
@@ -20,6 +20,7 @@ import _ignore from 'ignore'
20
20
  import { sort } from 'fast-sort'
21
21
  import { buildMigrateConfig } from '../migrate-config.js'
22
22
  import { loadYAML, dumpYAML } from '../adapters/yaml.js';
23
+ import { logger } from '../adapters/logger.js'
23
24
  import realFs from 'fs'
24
25
  import { preparePathForWriting, readFile, writeFile, stat, lstat, unlink } from '../adapters/file-gateway.js'
25
26
 
@@ -320,9 +321,16 @@ export function fileSystemStoreMixin(repo, fs = realFs) {
320
321
  if (!File.isFile(file)) throw new Error(ERRORS.NOT_A_FILE)
321
322
  var filePath = repo.getFullPath(file)
322
323
 
324
+ const readStartTime = Date.now()
325
+ logger.debug(`[INSTRUMENT] readFileContent START for ${filePath}`)
326
+ logger.debug(`[INSTRUMENT] About to stat ${filePath}`)
323
327
  const stats = await stat(filePath)
328
+ logger.debug(`[INSTRUMENT] stat took ${Date.now() - readStartTime}ms for ${filePath}`)
324
329
  if (!stats.isFile()) return file
330
+ const beforeReadTime = Date.now()
331
+ logger.debug(`[INSTRUMENT] About to readFile ${filePath}`)
325
332
  const data = await readFile(filePath, 'utf8')
333
+ logger.debug(`[INSTRUMENT] readFile took ${Date.now() - beforeReadTime}ms for ${filePath} (${data?.length || 0} chars)`)
326
334
 
327
335
  file
328
336
  .setContentFromFile(data)
@@ -330,6 +338,7 @@ export function fileSystemStoreMixin(repo, fs = realFs) {
330
338
  .setCreatedTime(stats.birthtime)
331
339
 
332
340
  file.lineCount = eol.split(data).length
341
+ logger.debug(`[INSTRUMENT] readFileContent END for ${filePath} total ${Date.now() - readStartTime}ms`)
333
342
  return file
334
343
  }
335
344
 
package/lib/project.js CHANGED
@@ -558,23 +558,33 @@ export class WorkerProject extends Project {
558
558
  }
559
559
 
560
560
  async addTaskToFile({path, list = this.config?.getDefaultList(), content, tags = [], contexts = [], meta = [], useCardTemplate = false}) {
561
+ logger.debug(`[INSTRUMENT] addTaskToFile START - path: ${path}, contentLength: ${content?.length || 0}`)
562
+
561
563
  const pluginMods = await this.pluginManager.onBeforeAddTask({path, list, meta, tags, contexts, content, useCardTemplate})
562
564
  path = pluginMods.path
563
565
  content = pluginMods.content
564
566
  meta = pluginMods.meta
565
567
  tags = pluginMods.tags
566
568
  contexts = pluginMods.contexts
569
+ logger.debug(`[INSTRUMENT] addTaskToFile - after onBeforeAddTask, contentLength: ${content?.length || 0}`)
570
+
567
571
  const cardData = await this.newCard({list, path, title: eol.split(content)[0], emit: false})
568
572
  const filePath = cardData.path
569
573
  if (useCardTemplate) content += cardData.template
574
+ logger.debug(`[INSTRUMENT] addTaskToFile - after newCard, contentLength: ${content?.length || 0}`)
570
575
 
571
576
  const boardData = await this.pluginManager.getBoardProperties();
572
577
  const data = { ...boardData, ...cardData}
578
+ logger.debug(`[INSTRUMENT] addTaskToFile - about to call format(), contentLength: ${content?.length || 0}`)
579
+ const formatStart = Date.now()
573
580
  content = format(content, data)
581
+ const formatTime = Date.now() - formatStart
582
+ logger.debug(`[INSTRUMENT] addTaskToFile - format() completed in ${formatTime}ms, contentLength: ${content?.length || 0}`)
574
583
 
575
584
  content = this.addTagsToContent(tags, content)
576
585
  content = this.addContextsToContent(contexts, content)
577
586
  content = this.addMetaToContent(meta, content)
587
+ logger.debug(`[INSTRUMENT] addTaskToFile - about to call repo.addTaskToFile, contentLength: ${content?.length || 0}`)
578
588
 
579
589
  return await this.repo.addTaskToFile(filePath, list, content)
580
590
  }
package/lib/repository.js CHANGED
@@ -1115,24 +1115,35 @@ export class Repository extends Emitter {
1115
1115
  // order:-80
1116
1116
  // -->
1117
1117
  async writeAndExtract (file, emit) {
1118
+ logger.debug(`[INSTRUMENT] writeAndExtract START - file: ${file.path}`)
1119
+
1118
1120
  try {
1121
+ logger.debug(`[INSTRUMENT] writeAndExtract - about to call writeFile`)
1119
1122
  await this.writeFile(file, emit)
1123
+ logger.debug(`[INSTRUMENT] writeAndExtract - writeFile completed`)
1120
1124
  } catch (err) {
1121
1125
  new Error('Unable to write file:' + file.path, {cause: err})
1122
1126
  }
1123
1127
 
1124
1128
  try {
1129
+ logger.debug(`[INSTRUMENT] writeAndExtract - about to call extractTasks`)
1130
+ const extractStart = Date.now()
1125
1131
  await this.extractTasks(file)
1132
+ const extractTime = Date.now() - extractStart
1133
+ logger.debug(`[INSTRUMENT] writeAndExtract - extractTasks completed in ${extractTime}ms`)
1126
1134
  } catch (err) {
1127
1135
  new Error('Unable to extract tasks for file:' + file.path, { cause: err })
1128
1136
  }
1129
1137
 
1130
1138
  try {
1139
+ logger.debug(`[INSTRUMENT] writeAndExtract - about to call addFile`)
1131
1140
  await this.addFile(file)
1141
+ logger.debug(`[INSTRUMENT] writeAndExtract - addFile completed`)
1132
1142
  } catch (err) {
1133
1143
  new Error('Unable to add file after extracting tasks: ' + file.path, { cause: err })
1134
1144
  }
1135
1145
 
1146
+ logger.debug(`[INSTRUMENT] writeAndExtract END`)
1136
1147
  return file
1137
1148
  }
1138
1149
 
@@ -1257,6 +1268,8 @@ export class Repository extends Emitter {
1257
1268
  // order:-300
1258
1269
  // -->
1259
1270
  async appendTask({file, content, list}) {
1271
+ logger.debug(`[INSTRUMENT] appendTask START - file: ${file.path}, contentLength: ${content?.length || 0}`)
1272
+
1260
1273
  const config = this.getConfig()
1261
1274
  const interpretedTaskPrefix = interpolate(
1262
1275
  config.getTaskPrefix(),
@@ -1267,6 +1280,8 @@ export class Repository extends Emitter {
1267
1280
  const taskSyntax = config.getNewCardSyntax()
1268
1281
  const taskPrefix = interpretedTaskPrefix ? `${interpretedTaskPrefix} ` : ''
1269
1282
  let fileContent = file.getContent()
1283
+ logger.debug(`[INSTRUMENT] appendTask - existing file contentLength: ${fileContent?.length || 0}`)
1284
+
1270
1285
  const journalTemplate = this.getConfig().journalTemplate
1271
1286
  fileContent = fileContent.trim()
1272
1287
  ? fileContent
@@ -1283,7 +1298,7 @@ export class Repository extends Emitter {
1283
1298
  const order = appContext().projectContext.getOrder(list)
1284
1299
  const description = lines.length > 1 ? Task.padDescription(lines.slice(1), taskPrefix) : []
1285
1300
  const { orderMeta, tokenPrefix } = config
1286
- const rawTask = getRawTask({tokenPrefix, orderMeta, list, order, text, type: taskSyntax})
1301
+ const rawTask = getRawTask({tokenPrefix, orderMeta, list, order, text, type: taskSyntax})
1287
1302
  const taskContent = config.orderMeta
1288
1303
  ? this.getTaskContent({
1289
1304
  description,
@@ -1297,13 +1312,20 @@ export class Repository extends Emitter {
1297
1312
  let appendContent = File.trimBlankLines(
1298
1313
  `${taskPrefix}${rawTask}${eol.lf}${taskContent}`
1299
1314
  )
1300
-
1315
+
1301
1316
  fileContent = `${fileContent}${sep}`
1302
1317
  const cardTerminator = "\n".repeat(2)
1303
- file.setContent(`${fileContent}${appendContent}${cardTerminator}`)
1318
+ const finalContent = `${fileContent}${appendContent}${cardTerminator}`;
1319
+ logger.debug(`[INSTRUMENT] appendTask - final contentLength: ${finalContent?.length || 0}`)
1320
+ logger.debug(`[INSTRUMENT] appendTask - about to call file.setContent`)
1321
+ file.setContent(finalContent)
1304
1322
 
1323
+ logger.debug(`[INSTRUMENT] appendTask - about to call writeAndExtract`)
1305
1324
  await this.writeAndExtract(file)
1325
+ logger.debug(`[INSTRUMENT] appendTask - writeAndExtract completed`)
1326
+
1306
1327
  const task = file.getTasks().find(task => task.text === text && task.list === list)
1328
+ logger.debug(`[INSTRUMENT] appendTask END - task found: ${!!task}`)
1307
1329
  return { file, task }
1308
1330
  }
1309
1331
 
@@ -1313,13 +1335,18 @@ export class Repository extends Emitter {
1313
1335
  // order:-290
1314
1336
  // -->
1315
1337
  async addTaskToFile (filePath, list, content) {
1338
+ logger.debug(`[INSTRUMENT] repo.addTaskToFile START - filePath: ${filePath}, contentLength: ${content?.length || 0}`)
1339
+
1316
1340
  const relPath = this.getRelativePath(filePath)
1317
1341
  let file = this.getFile(relPath)
1318
1342
 
1319
1343
  if (file) {
1344
+ logger.debug(`[INSTRUMENT] repo.addTaskToFile - file exists, reading content`)
1320
1345
  await this.readFileContent(file)
1346
+ logger.debug(`[INSTRUMENT] repo.addTaskToFile - about to call appendTask`)
1321
1347
  return await this.appendTask({file, content, list})
1322
1348
  } else {
1349
+ logger.debug(`[INSTRUMENT] repo.addTaskToFile - creating new file`)
1323
1350
  const modifiedTime = new Date()
1324
1351
  const createdTime = new Date()
1325
1352
  file = new File({
@@ -1330,6 +1357,7 @@ export class Repository extends Emitter {
1330
1357
  createdTime,
1331
1358
  project: this.project,
1332
1359
  })
1360
+ logger.debug(`[INSTRUMENT] repo.addTaskToFile - about to call appendTask for new file`)
1333
1361
  return await this.appendTask({file, content, list})
1334
1362
  }
1335
1363
  }
@@ -1545,4 +1573,4 @@ export class Repository extends Emitter {
1545
1573
  const result = Repository.query(this.getTasks(), queryString)
1546
1574
  return Repository.getTasksByList(this, result, true)
1547
1575
  }
1548
- }
1576
+ }
package/lib/task.js CHANGED
@@ -128,7 +128,7 @@ export class Task {
128
128
 
129
129
  getTextAndDescription() {
130
130
  const taskContent = `${this.markdownBeforeText}${this.text}${lineEnd}${this.descriptionString}`
131
- .replace(/<!--\s*\[\s*([\s\S]*?)\s*\]\s*-->/gm, '$1')
131
+ .replace(/<!--\s*\[\s*([\s\S]*?)\s*\]\s*-->/gm, '$1') // Safe: simple capture group, no backtracking issue
132
132
  return this.getText(
133
133
  {
134
134
  stripMeta: true,
@@ -199,13 +199,31 @@ export class Task {
199
199
  // order:-680
200
200
  // -->
201
201
  let prefix = newLine ? lineEnd : ' '
202
- const commentRegex = /<!--([\s\S]*?)-->/gm
203
- let result, lastIndex, index
204
- while ((result = commentRegex.exec(content))) {
205
- lastIndex = commentRegex.lastIndex
206
- index = result.index
202
+
203
+ // Find last comment using state machine (safe from backtracking)
204
+ let lastIndex = -1;
205
+ const len = content.length;
206
+ let i = 0;
207
+
208
+ while (i < len - 3) {
209
+ // Look for comment start <!--
210
+ if (content[i] === '<' && content[i + 1] === '!' && content[i + 2] === '-' && content[i + 3] === '-') {
211
+ i += 4;
212
+
213
+ // Find comment end -->
214
+ while (i < len - 2) {
215
+ if (content[i] === '-' && content[i + 1] === '-' && content[i + 2] === '>') {
216
+ i += 3;
217
+ lastIndex = i; // Remember end of this comment
218
+ break;
219
+ }
220
+ i++;
221
+ }
222
+ continue;
223
+ }
224
+ i++;
207
225
  }
208
-
226
+
209
227
  if (lastIndex > 0) {
210
228
  let before = content.slice(0, lastIndex - 3).trimEnd()
211
229
  const after = content.slice(lastIndex - 3)
@@ -434,7 +452,42 @@ export class Task {
434
452
  return linkLabelPositions.some(pos => index >= pos.start && index < pos.end);
435
453
  };
436
454
 
437
- static getCommentPositions (text) {
455
+ static getCommentPositions (text, useRegex = false) {
456
+ if (useRegex) {
457
+ return Task._getCommentPositionsRegex(text);
458
+ }
459
+
460
+ // State machine implementation (safe from catastrophic backtracking)
461
+ const positions = [];
462
+ const len = text.length;
463
+ let i = 0;
464
+
465
+ while (i < len - 3) { // Need at least 4 chars for <!--
466
+ // Look for comment start <!--
467
+ if (text[i] === '<' && text[i + 1] === '!' && text[i + 2] === '-' && text[i + 3] === '-') {
468
+ const start = i;
469
+ i += 4; // Skip <!--
470
+
471
+ // Find comment end -->
472
+ while (i < len - 2) {
473
+ if (text[i] === '-' && text[i + 1] === '-' && text[i + 2] === '>') {
474
+ i += 3; // Include -->
475
+ positions.push({ start, end: i });
476
+ break;
477
+ }
478
+ i++;
479
+ }
480
+ continue;
481
+ }
482
+ i++;
483
+ }
484
+
485
+ return positions;
486
+ }
487
+
488
+ static _getCommentPositionsRegex (text) {
489
+ // OLD IMPLEMENTATION - DO NOT USE IN PRODUCTION
490
+ // This regex can cause catastrophic backtracking with nested/malformed comments
438
491
  const positions = [];
439
492
  const re = /<!--[\s\S]*?-->/g;
440
493
  let match;
@@ -517,7 +570,58 @@ export class Task {
517
570
  return task instanceof Task
518
571
  }
519
572
 
520
- static getMarkdownCodePositions (text) {
573
+ static getMarkdownCodePositions (text, useRegex = false) {
574
+ if (useRegex) {
575
+ return Task._getMarkdownCodePositionsRegex(text);
576
+ }
577
+
578
+ // State machine implementation (safe from catastrophic backtracking)
579
+ const positions = [];
580
+ const len = text.length;
581
+ let i = 0;
582
+
583
+ while (i < len) {
584
+ // Check for code block (```)
585
+ if (text[i] === '`' && text[i + 1] === '`' && text[i + 2] === '`') {
586
+ const start = i;
587
+ i += 3;
588
+
589
+ while (i < len - 2) {
590
+ if (text[i] === '`' && text[i + 1] === '`' && text[i + 2] === '`') {
591
+ i += 3;
592
+ positions.push({ start, end: i });
593
+ break;
594
+ }
595
+ i++;
596
+ }
597
+ continue;
598
+ }
599
+
600
+ // Check for inline code (`)
601
+ if (text[i] === '`') {
602
+ const start = i;
603
+ i++;
604
+
605
+ while (i < len) {
606
+ if (text[i] === '`') {
607
+ i++;
608
+ positions.push({ start, end: i });
609
+ break;
610
+ }
611
+ i++;
612
+ }
613
+ continue;
614
+ }
615
+
616
+ i++;
617
+ }
618
+
619
+ return positions;
620
+ };
621
+
622
+ static _getMarkdownCodePositionsRegex (text) {
623
+ // OLD IMPLEMENTATION - DO NOT USE IN PRODUCTION
624
+ // This regex can cause catastrophic backtracking
521
625
  const re = /(```[\s\S]*?```|`[^`]+`)/g;
522
626
  let result;
523
627
  let positions = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imdone-core",
3
- "version": "2.1.6",
3
+ "version": "2.1.10",
4
4
  "description": "imdone-core",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Interpolates the content with the provided data.
2
+ * Interpolates the content with the provided data using state machine parsing.
3
3
  * @param {string} content - The content to interpolate.
4
4
  * @param {object} data - The data to use for interpolation.
5
5
  * @param {object} opts - The options for interpolation.
package/types/file.d.ts CHANGED
@@ -363,7 +363,11 @@ export class File extends Emitter<[never]> {
363
363
  createdTime: any;
364
364
  };
365
365
  isWithinCodeSpanOrBlock(pos: any, content: any): boolean;
366
- getMarkdownCodePositions(text: any): {
366
+ getMarkdownCodePositions(text: any, useRegex?: boolean): {
367
+ start: number;
368
+ end: number;
369
+ }[];
370
+ _getMarkdownCodePositionsRegex(text: any): {
367
371
  start: number;
368
372
  end: number;
369
373
  }[];
package/types/task.d.ts CHANGED
@@ -31,7 +31,11 @@ export class Task {
31
31
  end: number;
32
32
  }[];
33
33
  static isResultInMarkdownLinkLabel(linkLabelPositions: any, index: any): any;
34
- static getCommentPositions(text: any): {
34
+ static getCommentPositions(text: any, useRegex?: boolean): {
35
+ start: number;
36
+ end: number;
37
+ }[];
38
+ static _getCommentPositionsRegex(text: any): {
35
39
  start: number;
36
40
  end: number;
37
41
  }[];
@@ -49,7 +53,11 @@ export class Task {
49
53
  * @return BinaryExpression
50
54
  */
51
55
  static isTask(task: any): task is Task;
52
- static getMarkdownCodePositions(text: any): {
56
+ static getMarkdownCodePositions(text: any, useRegex?: boolean): {
57
+ start: number;
58
+ end: number;
59
+ }[];
60
+ static _getMarkdownCodePositionsRegex(text: any): {
53
61
  start: number;
54
62
  end: number;
55
63
  }[];