imdone-core 2.1.6 → 2.1.11
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 +60 -3
- package/lib/mixins/repo-fs-store.js +9 -0
- package/lib/project.js +10 -0
- package/lib/repository.js +32 -4
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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}, list: ${list}\n${content}`)
|
|
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
|
-
|
|
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
|
}[];
|