imdone-core 2.1.5 → 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.
- package/lib/adapters/parsers/content-transformer.js +254 -22
- package/lib/file.js +128 -7
- package/lib/mixins/repo-fs-store.js +9 -0
- package/lib/project.js +10 -0
- package/lib/repository.js +67 -5
- package/lib/task.js +113 -9
- package/package.json +1 -1
- package/types/adapters/parsers/content-transformer.d.ts +1 -1
- package/types/file.d.ts +5 -1
- package/types/task.d.ts +10 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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, (
|
|
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)
|
|
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
|
@@ -535,16 +535,38 @@ export class File extends Emitter {
|
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
537
|
extractTasks (config) {
|
|
538
|
+
// INSTRUMENTATION: Track timing for hang investigation
|
|
539
|
+
const startTime = Date.now()
|
|
540
|
+
log(`[INSTRUMENT] File.extractTasks START for ${this.path}`)
|
|
541
|
+
|
|
538
542
|
this.tasks = []
|
|
539
543
|
if (this.isMarkDownFile()) {
|
|
540
544
|
const data = this.parseFrontMatter(config)
|
|
541
|
-
if (data.imdone_ignore || data['kanban-plugin'])
|
|
545
|
+
if (data.imdone_ignore || data['kanban-plugin']) {
|
|
546
|
+
log(`[INSTRUMENT] File.extractTasks END (ignored) - ${Date.now() - startTime}ms`)
|
|
547
|
+
return this
|
|
548
|
+
}
|
|
542
549
|
}
|
|
550
|
+
|
|
551
|
+
const frontMatterTime = Date.now()
|
|
552
|
+
log(`[INSTRUMENT] Front matter check took ${frontMatterTime - startTime}ms`)
|
|
553
|
+
|
|
543
554
|
if (this.isCodeFile()) {
|
|
555
|
+
log(`[INSTRUMENT] About to extractTasksInCodeFile`)
|
|
544
556
|
this.extractTasksInCodeFile(config)
|
|
557
|
+
log(`[INSTRUMENT] extractTasksInCodeFile took ${Date.now() - frontMatterTime}ms`)
|
|
545
558
|
} else {
|
|
559
|
+
log(`[INSTRUMENT] About to extractTasksInNonCodeFile`)
|
|
546
560
|
this.extractTasksInNonCodeFile(config)
|
|
561
|
+
log(`[INSTRUMENT] extractTasksInNonCodeFile took ${Date.now() - frontMatterTime}ms`)
|
|
547
562
|
}
|
|
563
|
+
|
|
564
|
+
const totalTime = Date.now() - startTime
|
|
565
|
+
if (totalTime > 1000) {
|
|
566
|
+
log(`[INSTRUMENT] ⚠️ SLOW File.extractTasks: ${this.path} took ${totalTime}ms`)
|
|
567
|
+
}
|
|
568
|
+
log(`[INSTRUMENT] File.extractTasks END - total ${totalTime}ms`)
|
|
569
|
+
|
|
548
570
|
return this
|
|
549
571
|
}
|
|
550
572
|
|
|
@@ -560,20 +582,62 @@ export class File extends Emitter {
|
|
|
560
582
|
// order:40
|
|
561
583
|
// -->
|
|
562
584
|
extractTasksInCodeFile (config) {
|
|
585
|
+
// INSTRUMENTATION: Track regex iterations
|
|
586
|
+
const startTime = Date.now()
|
|
563
587
|
var self = this
|
|
564
588
|
var commentRegex = this.getCodeCommentRegex()
|
|
589
|
+
log(`[INSTRUMENT] getCodeCommentRegex took ${Date.now() - startTime}ms`)
|
|
590
|
+
|
|
565
591
|
var result
|
|
592
|
+
var iterationCount = 0
|
|
593
|
+
const contentLength = self.getContent()?.length || 0
|
|
594
|
+
log(`[INSTRUMENT] Starting regex loop, content length: ${contentLength}`)
|
|
595
|
+
|
|
566
596
|
while ((result = commentRegex.exec(self.getContent())) !== null) {
|
|
597
|
+
iterationCount++
|
|
598
|
+
if (iterationCount % 100 === 0) {
|
|
599
|
+
log(`[INSTRUMENT] Regex iteration ${iterationCount}, elapsed: ${Date.now() - startTime}ms`)
|
|
600
|
+
}
|
|
601
|
+
|
|
567
602
|
var comment = result[0]
|
|
568
603
|
var commentStart = result.index
|
|
569
604
|
self.extractCodeStyleTasks(config, commentStart, comment)
|
|
605
|
+
|
|
606
|
+
// Safety check for infinite loop
|
|
607
|
+
if (iterationCount > 10000) {
|
|
608
|
+
log(`[INSTRUMENT] ⚠️ WARNING: Exceeded 10000 regex iterations, breaking loop`)
|
|
609
|
+
break
|
|
610
|
+
}
|
|
570
611
|
}
|
|
612
|
+
|
|
613
|
+
const totalTime = Date.now() - startTime
|
|
614
|
+
log(`[INSTRUMENT] extractTasksInCodeFile END - ${iterationCount} iterations, ${totalTime}ms`)
|
|
571
615
|
}
|
|
572
616
|
|
|
573
617
|
extractTasksInNonCodeFile (config) {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
618
|
+
// INSTRUMENTATION: Track each extraction method
|
|
619
|
+
const startTime = Date.now()
|
|
620
|
+
const content = this.getContent()
|
|
621
|
+
const contentLength = content?.length || 0
|
|
622
|
+
log(`[INSTRUMENT] extractTasksInNonCodeFile START, content length: ${contentLength}`)
|
|
623
|
+
|
|
624
|
+
log(`[INSTRUMENT] About to extractHashStyleTasks`)
|
|
625
|
+
this.extractHashStyleTasks(config, 0, content)
|
|
626
|
+
const hashTime = Date.now()
|
|
627
|
+
log(`[INSTRUMENT] extractHashStyleTasks took ${hashTime - startTime}ms`)
|
|
628
|
+
|
|
629
|
+
log(`[INSTRUMENT] About to extractLinkStyleTasks`)
|
|
630
|
+
this.extractLinkStyleTasks(config, 0, content)
|
|
631
|
+
const linkTime = Date.now()
|
|
632
|
+
log(`[INSTRUMENT] extractLinkStyleTasks took ${linkTime - hashTime}ms`)
|
|
633
|
+
|
|
634
|
+
log(`[INSTRUMENT] About to extractCheckboxTasks`)
|
|
635
|
+
this.extractCheckboxTasks(config, 0, content)
|
|
636
|
+
const checkboxTime = Date.now()
|
|
637
|
+
log(`[INSTRUMENT] extractCheckboxTasks took ${checkboxTime - linkTime}ms`)
|
|
638
|
+
|
|
639
|
+
const totalTime = Date.now() - startTime
|
|
640
|
+
log(`[INSTRUMENT] extractTasksInNonCodeFile END - total ${totalTime}ms`)
|
|
577
641
|
}
|
|
578
642
|
|
|
579
643
|
deleteBlankLine (lineNo) {
|
|
@@ -1212,8 +1276,10 @@ export class File extends Emitter {
|
|
|
1212
1276
|
if (lang.block) {
|
|
1213
1277
|
var start = escapeRegExp(lang.block.start)
|
|
1214
1278
|
var end = escapeRegExp(lang.block.end)
|
|
1215
|
-
//
|
|
1216
|
-
|
|
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
|
|
1217
1283
|
}
|
|
1218
1284
|
|
|
1219
1285
|
return new RegExp(reString, 'gmi')
|
|
@@ -1364,7 +1430,62 @@ export class File extends Emitter {
|
|
|
1364
1430
|
return Task.isResultInMarkdownCode(positions, pos)
|
|
1365
1431
|
}
|
|
1366
1432
|
|
|
1367
|
-
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
|
|
1368
1489
|
const positions = [];
|
|
1369
1490
|
const codeBlockRe = /```[\s\S]*?```/g;
|
|
1370
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
|
@@ -755,20 +755,54 @@ export class Repository extends Emitter {
|
|
|
755
755
|
// order:-100
|
|
756
756
|
// -->
|
|
757
757
|
async extractTasks (file) {
|
|
758
|
+
// INSTRUMENTATION: Track timing for hang investigation
|
|
759
|
+
const extractStartTime = Date.now()
|
|
760
|
+
logger.debug(`[INSTRUMENT] extractTasks START for ${file.path}`)
|
|
761
|
+
|
|
758
762
|
if (file.content === null) {
|
|
759
763
|
await this.readFileContent(file)
|
|
760
764
|
}
|
|
765
|
+
|
|
766
|
+
const readTime = Date.now()
|
|
767
|
+
logger.debug(`[INSTRUMENT] readFileContent took ${readTime - extractStartTime}ms`)
|
|
768
|
+
|
|
761
769
|
file.on('task.found', this.taskFoundListener)
|
|
762
770
|
file.on('task.modified', this.taskModifiedListener)
|
|
763
771
|
const fileContent = file.content
|
|
772
|
+
|
|
773
|
+
logger.debug(`[INSTRUMENT] About to call extractAndTransformTasks, content length: ${fileContent?.length || 0}`)
|
|
764
774
|
file.extractAndTransformTasks(this.getConfig())
|
|
775
|
+
|
|
776
|
+
const transformTime = Date.now()
|
|
777
|
+
logger.debug(`[INSTRUMENT] extractAndTransformTasks took ${transformTime - readTime}ms`)
|
|
778
|
+
|
|
765
779
|
if (!file.isModified() || fileContent === file.content) {
|
|
766
780
|
this.resetFile(file)
|
|
781
|
+
const totalTime = Date.now() - extractStartTime
|
|
782
|
+
logger.debug(`[INSTRUMENT] extractTasks END (no changes) - total ${totalTime}ms`)
|
|
767
783
|
return file
|
|
768
784
|
}
|
|
785
|
+
|
|
786
|
+
logger.debug(`[INSTRUMENT] About to call file.extractTasks`)
|
|
769
787
|
file.extractTasks(this.getConfig())
|
|
770
|
-
|
|
788
|
+
|
|
789
|
+
const extractTime = Date.now()
|
|
790
|
+
logger.debug(`[INSTRUMENT] file.extractTasks took ${extractTime - transformTime}ms`)
|
|
791
|
+
|
|
792
|
+
if (file.modified) {
|
|
793
|
+
logger.debug(`[INSTRUMENT] About to writeFile`)
|
|
794
|
+
await this.writeFile(file)
|
|
795
|
+
logger.debug(`[INSTRUMENT] writeFile took ${Date.now() - extractTime}ms`)
|
|
796
|
+
}
|
|
797
|
+
|
|
771
798
|
this.resetFile(file)
|
|
799
|
+
|
|
800
|
+
const totalTime = Date.now() - extractStartTime
|
|
801
|
+
if (totalTime > 1000) {
|
|
802
|
+
logger.warn(`[INSTRUMENT] ⚠️ SLOW extractTasks: ${file.path} took ${totalTime}ms`)
|
|
803
|
+
}
|
|
804
|
+
logger.debug(`[INSTRUMENT] extractTasks END - total ${totalTime}ms`)
|
|
805
|
+
|
|
772
806
|
return file
|
|
773
807
|
}
|
|
774
808
|
|
|
@@ -1081,24 +1115,35 @@ export class Repository extends Emitter {
|
|
|
1081
1115
|
// order:-80
|
|
1082
1116
|
// -->
|
|
1083
1117
|
async writeAndExtract (file, emit) {
|
|
1118
|
+
logger.debug(`[INSTRUMENT] writeAndExtract START - file: ${file.path}`)
|
|
1119
|
+
|
|
1084
1120
|
try {
|
|
1121
|
+
logger.debug(`[INSTRUMENT] writeAndExtract - about to call writeFile`)
|
|
1085
1122
|
await this.writeFile(file, emit)
|
|
1123
|
+
logger.debug(`[INSTRUMENT] writeAndExtract - writeFile completed`)
|
|
1086
1124
|
} catch (err) {
|
|
1087
1125
|
new Error('Unable to write file:' + file.path, {cause: err})
|
|
1088
1126
|
}
|
|
1089
1127
|
|
|
1090
1128
|
try {
|
|
1129
|
+
logger.debug(`[INSTRUMENT] writeAndExtract - about to call extractTasks`)
|
|
1130
|
+
const extractStart = Date.now()
|
|
1091
1131
|
await this.extractTasks(file)
|
|
1132
|
+
const extractTime = Date.now() - extractStart
|
|
1133
|
+
logger.debug(`[INSTRUMENT] writeAndExtract - extractTasks completed in ${extractTime}ms`)
|
|
1092
1134
|
} catch (err) {
|
|
1093
1135
|
new Error('Unable to extract tasks for file:' + file.path, { cause: err })
|
|
1094
1136
|
}
|
|
1095
1137
|
|
|
1096
1138
|
try {
|
|
1139
|
+
logger.debug(`[INSTRUMENT] writeAndExtract - about to call addFile`)
|
|
1097
1140
|
await this.addFile(file)
|
|
1141
|
+
logger.debug(`[INSTRUMENT] writeAndExtract - addFile completed`)
|
|
1098
1142
|
} catch (err) {
|
|
1099
1143
|
new Error('Unable to add file after extracting tasks: ' + file.path, { cause: err })
|
|
1100
1144
|
}
|
|
1101
1145
|
|
|
1146
|
+
logger.debug(`[INSTRUMENT] writeAndExtract END`)
|
|
1102
1147
|
return file
|
|
1103
1148
|
}
|
|
1104
1149
|
|
|
@@ -1223,6 +1268,8 @@ export class Repository extends Emitter {
|
|
|
1223
1268
|
// order:-300
|
|
1224
1269
|
// -->
|
|
1225
1270
|
async appendTask({file, content, list}) {
|
|
1271
|
+
logger.debug(`[INSTRUMENT] appendTask START - file: ${file.path}, contentLength: ${content?.length || 0}`)
|
|
1272
|
+
|
|
1226
1273
|
const config = this.getConfig()
|
|
1227
1274
|
const interpretedTaskPrefix = interpolate(
|
|
1228
1275
|
config.getTaskPrefix(),
|
|
@@ -1233,6 +1280,8 @@ export class Repository extends Emitter {
|
|
|
1233
1280
|
const taskSyntax = config.getNewCardSyntax()
|
|
1234
1281
|
const taskPrefix = interpretedTaskPrefix ? `${interpretedTaskPrefix} ` : ''
|
|
1235
1282
|
let fileContent = file.getContent()
|
|
1283
|
+
logger.debug(`[INSTRUMENT] appendTask - existing file contentLength: ${fileContent?.length || 0}`)
|
|
1284
|
+
|
|
1236
1285
|
const journalTemplate = this.getConfig().journalTemplate
|
|
1237
1286
|
fileContent = fileContent.trim()
|
|
1238
1287
|
? fileContent
|
|
@@ -1249,7 +1298,7 @@ export class Repository extends Emitter {
|
|
|
1249
1298
|
const order = appContext().projectContext.getOrder(list)
|
|
1250
1299
|
const description = lines.length > 1 ? Task.padDescription(lines.slice(1), taskPrefix) : []
|
|
1251
1300
|
const { orderMeta, tokenPrefix } = config
|
|
1252
|
-
const rawTask = getRawTask({tokenPrefix, orderMeta, list, order, text, type: taskSyntax})
|
|
1301
|
+
const rawTask = getRawTask({tokenPrefix, orderMeta, list, order, text, type: taskSyntax})
|
|
1253
1302
|
const taskContent = config.orderMeta
|
|
1254
1303
|
? this.getTaskContent({
|
|
1255
1304
|
description,
|
|
@@ -1263,13 +1312,20 @@ export class Repository extends Emitter {
|
|
|
1263
1312
|
let appendContent = File.trimBlankLines(
|
|
1264
1313
|
`${taskPrefix}${rawTask}${eol.lf}${taskContent}`
|
|
1265
1314
|
)
|
|
1266
|
-
|
|
1315
|
+
|
|
1267
1316
|
fileContent = `${fileContent}${sep}`
|
|
1268
1317
|
const cardTerminator = "\n".repeat(2)
|
|
1269
|
-
|
|
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)
|
|
1270
1322
|
|
|
1323
|
+
logger.debug(`[INSTRUMENT] appendTask - about to call writeAndExtract`)
|
|
1271
1324
|
await this.writeAndExtract(file)
|
|
1325
|
+
logger.debug(`[INSTRUMENT] appendTask - writeAndExtract completed`)
|
|
1326
|
+
|
|
1272
1327
|
const task = file.getTasks().find(task => task.text === text && task.list === list)
|
|
1328
|
+
logger.debug(`[INSTRUMENT] appendTask END - task found: ${!!task}`)
|
|
1273
1329
|
return { file, task }
|
|
1274
1330
|
}
|
|
1275
1331
|
|
|
@@ -1279,13 +1335,18 @@ export class Repository extends Emitter {
|
|
|
1279
1335
|
// order:-290
|
|
1280
1336
|
// -->
|
|
1281
1337
|
async addTaskToFile (filePath, list, content) {
|
|
1338
|
+
logger.debug(`[INSTRUMENT] repo.addTaskToFile START - filePath: ${filePath}, contentLength: ${content?.length || 0}`)
|
|
1339
|
+
|
|
1282
1340
|
const relPath = this.getRelativePath(filePath)
|
|
1283
1341
|
let file = this.getFile(relPath)
|
|
1284
1342
|
|
|
1285
1343
|
if (file) {
|
|
1344
|
+
logger.debug(`[INSTRUMENT] repo.addTaskToFile - file exists, reading content`)
|
|
1286
1345
|
await this.readFileContent(file)
|
|
1346
|
+
logger.debug(`[INSTRUMENT] repo.addTaskToFile - about to call appendTask`)
|
|
1287
1347
|
return await this.appendTask({file, content, list})
|
|
1288
1348
|
} else {
|
|
1349
|
+
logger.debug(`[INSTRUMENT] repo.addTaskToFile - creating new file`)
|
|
1289
1350
|
const modifiedTime = new Date()
|
|
1290
1351
|
const createdTime = new Date()
|
|
1291
1352
|
file = new File({
|
|
@@ -1296,6 +1357,7 @@ export class Repository extends Emitter {
|
|
|
1296
1357
|
createdTime,
|
|
1297
1358
|
project: this.project,
|
|
1298
1359
|
})
|
|
1360
|
+
logger.debug(`[INSTRUMENT] repo.addTaskToFile - about to call appendTask for new file`)
|
|
1299
1361
|
return await this.appendTask({file, content, list})
|
|
1300
1362
|
}
|
|
1301
1363
|
}
|
|
@@ -1511,4 +1573,4 @@ export class Repository extends Emitter {
|
|
|
1511
1573
|
const result = Repository.query(this.getTasks(), queryString)
|
|
1512
1574
|
return Repository.getTasksByList(this, result, true)
|
|
1513
1575
|
}
|
|
1514
|
-
}
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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,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
|
}[];
|