@gitsense/gsc-utils 0.1.0
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/LICENSE +21 -0
- package/dist/gitsense-chat-utils.cjs.js +10977 -0
- package/dist/gitsense-chat-utils.esm.js +10975 -0
- package/dist/gsc-utils.cjs.js +11043 -0
- package/dist/gsc-utils.esm.js +11041 -0
- package/package.json +37 -0
- package/src/AnalysisBlockUtils.js +151 -0
- package/src/ChatUtils.js +126 -0
- package/src/CodeBlockUtils/blockExtractor.js +277 -0
- package/src/CodeBlockUtils/blockProcessor.js +559 -0
- package/src/CodeBlockUtils/blockProcessor.js.rej +8 -0
- package/src/CodeBlockUtils/constants.js +62 -0
- package/src/CodeBlockUtils/continuationUtils.js +191 -0
- package/src/CodeBlockUtils/headerParser.js +175 -0
- package/src/CodeBlockUtils/headerUtils.js +236 -0
- package/src/CodeBlockUtils/index.js +83 -0
- package/src/CodeBlockUtils/lineNumberFormatter.js +117 -0
- package/src/CodeBlockUtils/markerRemover.js +89 -0
- package/src/CodeBlockUtils/patchIntegration.js +38 -0
- package/src/CodeBlockUtils/relationshipUtils.js +159 -0
- package/src/CodeBlockUtils/updateCodeBlock.js +372 -0
- package/src/CodeBlockUtils/uuidUtils.js +48 -0
- package/src/ContextUtils.js +180 -0
- package/src/GSToolBlockUtils.js +108 -0
- package/src/GitSenseChatUtils.js +386 -0
- package/src/JsonUtils.js +101 -0
- package/src/LLMUtils.js +31 -0
- package/src/MessageUtils.js +460 -0
- package/src/PatchUtils/constants.js +72 -0
- package/src/PatchUtils/diagnosticReporter.js +213 -0
- package/src/PatchUtils/enhancedPatchProcessor.js +390 -0
- package/src/PatchUtils/fuzzyMatcher.js +252 -0
- package/src/PatchUtils/hunkCorrector.js +204 -0
- package/src/PatchUtils/hunkValidator.js +305 -0
- package/src/PatchUtils/index.js +135 -0
- package/src/PatchUtils/patchExtractor.js +175 -0
- package/src/PatchUtils/patchHeaderFormatter.js +143 -0
- package/src/PatchUtils/patchParser.js +289 -0
- package/src/PatchUtils/patchProcessor.js +389 -0
- package/src/PatchUtils/patchVerifier/constants.js +23 -0
- package/src/PatchUtils/patchVerifier/detectAndFixOverlappingHunks.js +281 -0
- package/src/PatchUtils/patchVerifier/detectAndFixRedundantChanges.js +404 -0
- package/src/PatchUtils/patchVerifier/formatAndAddLineNumbers.js +165 -0
- package/src/PatchUtils/patchVerifier/index.js +25 -0
- package/src/PatchUtils/patchVerifier/verifyAndCorrectHunkHeaders.js +202 -0
- package/src/PatchUtils/patchVerifier/verifyAndCorrectLineNumbers.js +254 -0
- package/src/SharedUtils/timestampUtils.js +41 -0
- package/src/SharedUtils/versionUtils.js +58 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gitsense/gsc-utils",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Utilities for GitSense Chat (GSC)",
|
|
5
|
+
"main": "dist/gsc-utils.cjs.js",
|
|
6
|
+
"module": "dist/gsc-utils.esm.js",
|
|
7
|
+
"browser": "dist/gsc-utils.cjs.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"src",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "rollup -c",
|
|
17
|
+
"dev": "rollup -c -w",
|
|
18
|
+
"test": "jest",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"git",
|
|
23
|
+
"chat",
|
|
24
|
+
"code-blocks",
|
|
25
|
+
"patches",
|
|
26
|
+
"markdown",
|
|
27
|
+
"parsing"
|
|
28
|
+
],
|
|
29
|
+
"author": "GitSense Team",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@rollup/plugin-commonjs": "^25.0.7",
|
|
32
|
+
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
33
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
34
|
+
"jest": "^29.7.0",
|
|
35
|
+
"rollup": "^4.9.6"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component: AnalysisBlockUtils
|
|
3
|
+
* Block-UUID: df4a5c56-bcee-4e83-989f-056b0d20005b
|
|
4
|
+
* Parent-UUID: 2a1b8c5d-e0f3-4a1c-8d7e-5e6f0a9b1c2e
|
|
5
|
+
* Version: 1.2.0
|
|
6
|
+
* Description: Provides utilities for identifying, parsing, and validating structured analysis blocks, including specific types like Overview (Short/Long/Tiny). These blocks typically follow a specific Markdown format generated by LLMs.
|
|
7
|
+
* Language: JavaScript
|
|
8
|
+
* Created-at: 2025-04-20T04:26:47.000Z
|
|
9
|
+
* Authors: Gemini 2.5 Pro (v1.0.0), Gemini 2.5 Pro (v1.1.0), Gemini 2.5 Flash Thinking (v1.2.0)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Checks for the `# Gitsense Chat Analysis` line
|
|
15
|
+
* @param {string} content - The raw content string to check.
|
|
16
|
+
* @returns {boolean} True if the content starts with the overview headers, false otherwise.
|
|
17
|
+
*/
|
|
18
|
+
function isAnalysisBlock(content) {
|
|
19
|
+
if (typeof content !== 'string') {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const trimmedContent = content.trimStart();
|
|
24
|
+
return trimmedContent.startsWith('# GitSense Chat Analysis\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Determines the type of overview block based on its starting header.
|
|
29
|
+
* @param {string} content - The raw content string.
|
|
30
|
+
* @returns analysis block type or null if not recognized.
|
|
31
|
+
*/
|
|
32
|
+
function getAnalysisBlockType(content) {
|
|
33
|
+
if (typeof content !== 'string') {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const lines = content.split('\n');
|
|
37
|
+
|
|
38
|
+
// The first header 2 line contains the analyze type
|
|
39
|
+
const typeLine = lines.find(line => line.startsWith('## ')) || '';
|
|
40
|
+
|
|
41
|
+
if (typeLine)
|
|
42
|
+
return typeLine.trim().split('## ').pop().toLowerCase();
|
|
43
|
+
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parses the metadata from a structured analysis block content.
|
|
49
|
+
* Assumes metadata is presented as a Markdown list: '* **Field:** Value'.
|
|
50
|
+
* Handles single-line values primarily. Multi-line values might require adjustments.
|
|
51
|
+
* @param {string} content - The raw content of the analysis block.
|
|
52
|
+
* @returns {Object | null} An object containing the extracted metadata fields (e.g., { 'Chat ID': '...', 'Repository': '...' }) or null if parsing fails or no fields are found.
|
|
53
|
+
*/
|
|
54
|
+
function parseOverviewMetadata(content) {
|
|
55
|
+
if (!isAnalysisBlock(content)) {
|
|
56
|
+
return null; // Not an overview block
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const metadata = {};
|
|
60
|
+
const lines = content.split('\n');
|
|
61
|
+
|
|
62
|
+
// Regex to capture "Field Name" and "Value" from lines like '* **Field Name: ** Value'
|
|
63
|
+
const fieldRegex = /^\*\s+\*\*([\w\s.-]+):\*\*\s+(.*)$/;
|
|
64
|
+
let foundList = false;
|
|
65
|
+
let currentKey = null; // For potential multi-line handling in the future
|
|
66
|
+
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const trimmedLine = line.trim();
|
|
69
|
+
|
|
70
|
+
// Skip empty lines before the list starts
|
|
71
|
+
if (!trimmedLine && !foundList) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// If we found the list and encounter an empty line, assume the list ended.
|
|
75
|
+
if (!trimmedLine && foundList) {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const match = trimmedLine.match(fieldRegex);
|
|
80
|
+
|
|
81
|
+
if (match) {
|
|
82
|
+
foundList = true;
|
|
83
|
+
const key = match[1].trim(); // e.g., "Chat ID", "Summarized At"
|
|
84
|
+
const value = match[2].trim();
|
|
85
|
+
metadata[key] = key.includes('ID') ? parseInt(value) : value;
|
|
86
|
+
currentKey = key; // Track the last successfully parsed key
|
|
87
|
+
} else if (foundList) {
|
|
88
|
+
// FIXME: This is currently not supported. Should we remove it?
|
|
89
|
+
//
|
|
90
|
+
// Handle potential multi-line values (simple append approach)
|
|
91
|
+
// Only append if the line doesn't look like another list item or header
|
|
92
|
+
if (currentKey && !trimmedLine.startsWith('*') && !trimmedLine.startsWith('#')) {
|
|
93
|
+
// Check if the field is one known to potentially be multi-line
|
|
94
|
+
const multiLineFields = ['Summary', 'Key Functionality', 'Keywords'];
|
|
95
|
+
if (multiLineFields.includes(currentKey)) {
|
|
96
|
+
metadata[currentKey] += '\n' + trimmedLine; // Append the raw trimmed line
|
|
97
|
+
} else {
|
|
98
|
+
// If not a known multi-line field, assume the list ended here.
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
// If it looks like another list item or something else, stop parsing this block's list.
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Ignore lines before the list starts that don't match the pattern
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Basic check if any metadata was actually parsed
|
|
110
|
+
if (Object.keys(metadata).length === 0) {
|
|
111
|
+
// console.warn("AnalysisBlockUtils: Could not parse any metadata fields from the block.");
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return metadata;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Validates the parsed analysis metadata object.
|
|
120
|
+
* Checks for the presence and basic type of required fields.
|
|
121
|
+
* @param {Object | null} metadata - The metadata object returned by parseOverviewMetadata.
|
|
122
|
+
* @returns {{isValid: boolean, errors: Array<string>}} Validation result.
|
|
123
|
+
*/
|
|
124
|
+
function validateAnalysisMetadata(metadata) {
|
|
125
|
+
const errors = [];
|
|
126
|
+
const requiredFields = [ 'Chat ID' ];
|
|
127
|
+
|
|
128
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
129
|
+
return { isValid: false, errors: ['Metadata object is null or invalid.'] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check for presence and non-empty string values
|
|
133
|
+
for (const field of requiredFields) {
|
|
134
|
+
if (!metadata[field]) {
|
|
135
|
+
errors.push(`Missing or empty required field: "${field}".`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
isValid: errors.length === 0,
|
|
141
|
+
errors: errors
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
isAnalysisBlock,
|
|
148
|
+
getAnalysisBlockType,
|
|
149
|
+
parseOverviewMetadata,
|
|
150
|
+
validateAnalysisMetadata
|
|
151
|
+
};
|
package/src/ChatUtils.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component: ChatUtils
|
|
3
|
+
* Block-UUID: fdbd7f40-a1ee-4c07-bbfe-bbac53efd2c8
|
|
4
|
+
* Parent-UUID: c981830e-3472-4d2b-b6d4-37b8e661ef4c
|
|
5
|
+
* Version: 1.3.0
|
|
6
|
+
* Description: Provides utility functions for analyzing and classifying chat sessions based on message content and structure.
|
|
7
|
+
* Language: JavaScript
|
|
8
|
+
* Created-at: 2025-07-11T19:54:25.519Z
|
|
9
|
+
* Authors: Gemini 2.5 Pro (v1.0.0), Gemini 2.5 Pro (v1.1.0), Gemini 2.5 Flash Thinking (v1.2.0), Gemini 2.5 Flash Thinking (v1.2.1), Gemini 2.5 Flash Thinking (v1.3.0)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const { getMessagesBeforeId } = require('./MessageUtils');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Internal helper function to determine if a chat session matches a specific pattern.
|
|
17
|
+
* Checks message count, structure, and specific starting strings for the first two assistant messages.
|
|
18
|
+
* @param {object} chat - The chat object, expected to have `chat.messages[0]` containing the message array.
|
|
19
|
+
* @param {string} model - The model.
|
|
20
|
+
* @param {string} firstAssistantMsgStart - The string the first assistant message must start with.
|
|
21
|
+
* @returns {boolean} True if the chat matches the pattern, false otherwise.
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
function checkChatTypePattern(chat, model, firstAssistantMsgStart) {
|
|
25
|
+
if (!chat || !Array.isArray(chat.messages) || !chat.messages.length) {
|
|
26
|
+
console.error("ChatUtils: Invalid chat or message structure provided.");
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const messages = getChatMessages(chat, model);
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* Common checks for chat type classification:
|
|
34
|
+
* 1) Must be 4 or more messages.
|
|
35
|
+
* 2) First assistant message must start with a specific string (passed as parameter).
|
|
36
|
+
* 3) Second assistant message must be instructions or context.
|
|
37
|
+
* - Instructions start with '# User Instructions\n'
|
|
38
|
+
* - Context starts with '## FILE CONTENT - ' or '## OVERVIEW - '
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
// Filter assistant messages
|
|
42
|
+
const assistantMessages = messages.filter(m => m.role === 'assistant');
|
|
43
|
+
|
|
44
|
+
// First assistant message check
|
|
45
|
+
if (!assistantMessages[0].message || !assistantMessages[0].message.startsWith(firstAssistantMsgStart)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Determines if the current chat session appears to be a "New Analyzer" session.
|
|
55
|
+
* @param {object} chat - The chat object.
|
|
56
|
+
* @returns {boolean} True if the chat is of type 'new-analyzer'
|
|
57
|
+
*/
|
|
58
|
+
function isNewAnalyzerChat(chat, model) {
|
|
59
|
+
return chat.type === 'new-analyzer';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Determines if the current chat session appears to be an "Analyze" session.
|
|
64
|
+
* @param {object} chat - The chat object.
|
|
65
|
+
* @param {string} model - Optional model.
|
|
66
|
+
* @returns {boolean} True if the chat matches the Overview Builder pattern, false otherwise.
|
|
67
|
+
*/
|
|
68
|
+
function isAnalyzeChat(chat, model) {
|
|
69
|
+
return chat.type === 'analyze';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Determines if the current chat session appears to be an "Ask" session.
|
|
74
|
+
* @param {object} chat - The chat object.
|
|
75
|
+
* @param {string} model - Optional model.
|
|
76
|
+
* @returns {boolean} True if the chat matches the Ask pattern, false otherwise.
|
|
77
|
+
*/
|
|
78
|
+
function isAskChat(chat, model) {
|
|
79
|
+
return checkChatTypePattern(chat, model || chat.main_model, '# Ask\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Determines if the current chat session appears to be a "Plan Chat" session.
|
|
84
|
+
* @param {object} chat - The chat object.
|
|
85
|
+
* @param {string} model - Optional model.
|
|
86
|
+
* @returns {boolean} True if the chat matches the Plan Chat pattern, false otherwise.
|
|
87
|
+
*/
|
|
88
|
+
function isPlanChat(chat, model) {
|
|
89
|
+
// Note: Original code checked for '# Plan'. If '# Context Builder Assistant' was intended, update the string below.
|
|
90
|
+
return checkChatTypePattern(chat, model || chat.main_model, '# Plan\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Determines if the current chat session appears to be a "Code Chat" session.
|
|
95
|
+
* @param {object} chat - The chat object.
|
|
96
|
+
* @param {string} model - Optional model.
|
|
97
|
+
* @returns {boolean} True if the chat matches the Code Chat pattern, false otherwise.
|
|
98
|
+
*/
|
|
99
|
+
function isCodeChat(chat, model) {
|
|
100
|
+
// Note: Original code checked for '# Coding Assistant'. If '# Code - Coding Assistant' was intended, update the string below.
|
|
101
|
+
return checkChatTypePattern(chat, model || chat.main_model, '# Code\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Retrieves all messages from a chat session.
|
|
106
|
+
* @param {object} chat - The chat object, expected to have `chat.messages[0]` containing the message array.
|
|
107
|
+
* @param {string} model - Optional model.
|
|
108
|
+
* @returns {Array<object>} An array of message objects, or an empty array if chat or messages are invalid.
|
|
109
|
+
*/
|
|
110
|
+
function getChatMessages(chat, model) {
|
|
111
|
+
if (!chat || !Array.isArray(chat.messages) || !chat.messages.length) {
|
|
112
|
+
console.error("ChatUtils: Invalid chat or message structure provided for getChatMessages.");
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
// Setting before id to null will retrieve all messages
|
|
116
|
+
return getMessagesBeforeId(model || chat.main_model, chat.messages[0], null);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
isAskChat,
|
|
121
|
+
isNewAnalyzerChat,
|
|
122
|
+
isAnalyzeChat,
|
|
123
|
+
isPlanChat,
|
|
124
|
+
isCodeChat,
|
|
125
|
+
getChatMessages
|
|
126
|
+
};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component: CodeBlockUtils Block Extractor
|
|
3
|
+
* Block-UUID: efacd230-2ced-4910-a417-efc536ca4c68
|
|
4
|
+
* Parent-UUID: 4b1a9f2c-8d3e-4c5a-9b1f-0e2d7a3c8b4d
|
|
5
|
+
* Version: 1.2.0
|
|
6
|
+
* Description: Provides functions to find and match markdown code fences (```) to identify block boundaries, and extract blocks with UUIDs.
|
|
7
|
+
* Language: JavaScript
|
|
8
|
+
* Created-at: 2025-04-15T15:55:49.293Z
|
|
9
|
+
* Authors: Gemini 2.5 Pro (v1.0.0), Gemini 2.5 Pro (v1.1.0), Gemini 2.5 Flash Thinking (v1.2.0)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Finds all opening and closing code fence positions using a line-by-line approach
|
|
15
|
+
* @param {string} text - The input text
|
|
16
|
+
* @returns {Object} Object containing arrays of opening and closing fence positions: { openingPositions: Array, closingPositions: Array }
|
|
17
|
+
*/
|
|
18
|
+
function findAllCodeFences(text) {
|
|
19
|
+
const openingPositions = [];
|
|
20
|
+
const closingPositions = [];
|
|
21
|
+
|
|
22
|
+
// Split text into lines for processing
|
|
23
|
+
const lines = text.split('\n');
|
|
24
|
+
let inCodeBlock = false;
|
|
25
|
+
let currentOpeningFence = null;
|
|
26
|
+
let linePosition = 0;
|
|
27
|
+
|
|
28
|
+
// Process each line
|
|
29
|
+
for (let i = 0; i < lines.length; i++) {
|
|
30
|
+
const line = lines[i];
|
|
31
|
+
const trimmedLine = line.trim();
|
|
32
|
+
|
|
33
|
+
// Check for opening fence (only if not already in a code block)
|
|
34
|
+
if (!inCodeBlock) {
|
|
35
|
+
// Match opening fence with or without language specifier
|
|
36
|
+
const openingMatch = trimmedLine.match(/^```([a-zA-Z-]*)$/);
|
|
37
|
+
if (openingMatch) {
|
|
38
|
+
inCodeBlock = true;
|
|
39
|
+
const fencePosition = linePosition + line.indexOf('```');
|
|
40
|
+
currentOpeningFence = {
|
|
41
|
+
position: fencePosition,
|
|
42
|
+
language: openingMatch[1].trim().toLowerCase() || "unknown",
|
|
43
|
+
length: openingMatch[0].length
|
|
44
|
+
};
|
|
45
|
+
openingPositions.push(currentOpeningFence);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Check for closing fence (only if in a code block)
|
|
49
|
+
else if (inCodeBlock) {
|
|
50
|
+
// Match closing fence (exactly ```)
|
|
51
|
+
if (trimmedLine === '```') {
|
|
52
|
+
inCodeBlock = false;
|
|
53
|
+
const fencePosition = linePosition + line.indexOf('```');
|
|
54
|
+
closingPositions.push({
|
|
55
|
+
position: fencePosition,
|
|
56
|
+
length: 3
|
|
57
|
+
});
|
|
58
|
+
currentOpeningFence = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Update line position for next iteration
|
|
63
|
+
linePosition += line.length + 1; // +1 for the newline character
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// If we reach the end and still have an open code block, it's incomplete
|
|
67
|
+
// We don't add a closing position for it
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
openingPositions,
|
|
71
|
+
closingPositions
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Matches opening and closing fences to identify complete and incomplete blocks
|
|
78
|
+
* @param {string} text - The input text
|
|
79
|
+
* @param {Array} openingPositions - Array of opening fence objects { position, language, length }
|
|
80
|
+
* @param {Array} closingPositions - Array of closing fence objects { position, length }
|
|
81
|
+
* @returns {Object} Object containing: { completeBlocks: Array, incompleteBlocks: Array, warnings: Array }
|
|
82
|
+
*/
|
|
83
|
+
function matchFencesAndExtractBlocks(text, openingPositions, closingPositions) {
|
|
84
|
+
const completeBlocks = [];
|
|
85
|
+
const incompleteBlocks = [];
|
|
86
|
+
const warnings = [];
|
|
87
|
+
|
|
88
|
+
// Create a copy of closing positions that we can modify
|
|
89
|
+
const availableClosingPositions = [...closingPositions];
|
|
90
|
+
|
|
91
|
+
// Sort opening and closing positions by their position in the text
|
|
92
|
+
const sortedOpenings = [...openingPositions].sort((a, b) => a.position - b.position);
|
|
93
|
+
availableClosingPositions.sort((a, b) => a.position - b.position);
|
|
94
|
+
|
|
95
|
+
// Process all opening fences in order
|
|
96
|
+
let openingIndex = 0;
|
|
97
|
+
let closingIndex = 0; // Tracks the next available closing fence to consider
|
|
98
|
+
|
|
99
|
+
while (openingIndex < sortedOpenings.length) {
|
|
100
|
+
const currentOpening = sortedOpenings[openingIndex];
|
|
101
|
+
|
|
102
|
+
// Find the next closing fence that comes *after* this opening fence ends
|
|
103
|
+
let matchingClosingIndex = -1;
|
|
104
|
+
for (let i = closingIndex; i < availableClosingPositions.length; i++) {
|
|
105
|
+
if (availableClosingPositions[i].position > currentOpening.position + currentOpening.length -1) { // Ensure closing is strictly after opening
|
|
106
|
+
matchingClosingIndex = i;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (matchingClosingIndex !== -1) {
|
|
112
|
+
// Found a potential matching closing fence
|
|
113
|
+
const matchingClosing = availableClosingPositions[matchingClosingIndex];
|
|
114
|
+
|
|
115
|
+
// Check if there's another opening fence before this closing fence
|
|
116
|
+
let nestedOpeningFound = false;
|
|
117
|
+
if (openingIndex + 1 < sortedOpenings.length) {
|
|
118
|
+
if (sortedOpenings[openingIndex + 1].position < matchingClosing.position) {
|
|
119
|
+
// This indicates a likely nested structure or potentially malformed input.
|
|
120
|
+
// For simplicity in this extractor, we'll treat the current opening as incomplete
|
|
121
|
+
// if a new opening starts before its potential closer.
|
|
122
|
+
// A more sophisticated parser might handle nesting.
|
|
123
|
+
// nestedOpeningFound = true; // Uncomment if handling nesting differently
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// If not nested (or handling nesting differently), proceed
|
|
128
|
+
if (!nestedOpeningFound) {
|
|
129
|
+
// Check if there's any content between the fences (excluding only whitespace)
|
|
130
|
+
const blockContent = text.substring(
|
|
131
|
+
currentOpening.position + currentOpening.length,
|
|
132
|
+
matchingClosing.position
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Only consider it a valid block if there's non-whitespace content or it's explicitly empty
|
|
136
|
+
if (blockContent.trim().length > 0 || blockContent.length === 0) { // Allow empty blocks like ```\n```
|
|
137
|
+
completeBlocks.push({
|
|
138
|
+
opening: currentOpening,
|
|
139
|
+
closing: matchingClosing,
|
|
140
|
+
incomplete: false
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Consume this closing fence and advance the closing index
|
|
144
|
+
closingIndex = matchingClosingIndex + 1;
|
|
145
|
+
} else {
|
|
146
|
+
// Block contains only whitespace - treat as potentially malformed or skip
|
|
147
|
+
warnings.push({
|
|
148
|
+
position: currentOpening.position,
|
|
149
|
+
type: 'whitespace_only_block',
|
|
150
|
+
message: `Code block starting at position ${currentOpening.position} contains only whitespace.`
|
|
151
|
+
});
|
|
152
|
+
// Don't consume the closing fence yet, let the next opening try to pair with it
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// Handle nested/malformed case - treat current opening as incomplete for now
|
|
156
|
+
incompleteBlocks.push({
|
|
157
|
+
opening: currentOpening,
|
|
158
|
+
incomplete: true
|
|
159
|
+
});
|
|
160
|
+
warnings.push({
|
|
161
|
+
position: currentOpening.position,
|
|
162
|
+
type: 'potentially_nested_or_malformed',
|
|
163
|
+
message: `Potentially nested or malformed block structure starting at position ${currentOpening.position}. Treating as incomplete.`
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
} else {
|
|
168
|
+
// No matching closing fence found for this opening fence - this is an incomplete block
|
|
169
|
+
// Check if there's actual content after the opening fence
|
|
170
|
+
const remainingContent = text.substring(currentOpening.position + currentOpening.length);
|
|
171
|
+
if (remainingContent.trim().length > 0) {
|
|
172
|
+
incompleteBlocks.push({
|
|
173
|
+
opening: currentOpening,
|
|
174
|
+
incomplete: true
|
|
175
|
+
});
|
|
176
|
+
warnings.push({
|
|
177
|
+
position: currentOpening.position,
|
|
178
|
+
type: 'incomplete_block',
|
|
179
|
+
message: `Incomplete code block found starting at position ${currentOpening.position}. Missing closing fence.`
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
// Opening fence at the very end of the file with nothing after it. Ignore.
|
|
183
|
+
warnings.push({
|
|
184
|
+
position: currentOpening.position,
|
|
185
|
+
type: 'trailing_opening_fence',
|
|
186
|
+
message: `Opening code fence found at the end of the text with no content after it at position ${currentOpening.position}.`
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Move to the next opening fence
|
|
192
|
+
openingIndex++;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check for any remaining closing fences that weren't matched (might indicate ``` outside blocks)
|
|
196
|
+
if (closingIndex < availableClosingPositions.length) {
|
|
197
|
+
for (let i = closingIndex; i < availableClosingPositions.length; i++) {
|
|
198
|
+
warnings.push({
|
|
199
|
+
position: availableClosingPositions[i].position,
|
|
200
|
+
type: 'unmatched_closing_fence',
|
|
201
|
+
message: `Found an unmatched closing code fence at position ${availableClosingPositions[i].position}.`
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
return { completeBlocks, incompleteBlocks, warnings };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extracts all code blocks with their Block-UUIDs from a message
|
|
212
|
+
* @param {string} messageText - The message text
|
|
213
|
+
* @returns {Array} Array of objects with code, language, blockUUID, startIndex, endIndex
|
|
214
|
+
*/
|
|
215
|
+
function extractCodeBlocksWithUUIDs(messageText) {
|
|
216
|
+
if (!messageText || typeof messageText !== 'string') {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const codeBlocks = [];
|
|
221
|
+
// Improved regex to handle various code block formats
|
|
222
|
+
const codeBlockRegex = /```(\w+)?\s*\n([\s\S]*?)```/g;
|
|
223
|
+
|
|
224
|
+
let match;
|
|
225
|
+
while ((match = codeBlockRegex.exec(messageText)) !== null) {
|
|
226
|
+
const language = match[1] ? match[1].trim() : 'text';
|
|
227
|
+
const code = match[2]; // Keep original spacing/newlines within block
|
|
228
|
+
|
|
229
|
+
// Extract Block-UUID from code if present - more flexible pattern
|
|
230
|
+
let blockUUID = null;
|
|
231
|
+
// Match UUID in potential header section (look near the top)
|
|
232
|
+
const lines = code.split('\n');
|
|
233
|
+
const maxHeaderLines = 20; // Limit search to avoid scanning huge blocks
|
|
234
|
+
for (let i = 0; i < Math.min(lines.length, maxHeaderLines); i++) {
|
|
235
|
+
const uuidMatch = code.match(/Block-UUID:\s*([a-f0-9-]+)/i);
|
|
236
|
+
if (uuidMatch && uuidMatch[1]) {
|
|
237
|
+
blockUUID = uuidMatch[1];
|
|
238
|
+
break; // Found it
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
codeBlocks.push({
|
|
244
|
+
language,
|
|
245
|
+
code, // Raw code content
|
|
246
|
+
blockUUID,
|
|
247
|
+
startIndex: match.index,
|
|
248
|
+
endIndex: match.index + match[0].length
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return codeBlocks;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Finds a code block by UUID in message text
|
|
257
|
+
* (Moved from original PatchUtils)
|
|
258
|
+
* @param {string} messageText - The message text
|
|
259
|
+
* @param {string} blockUUID - The Block-UUID to find
|
|
260
|
+
* @returns {Object|null} Code block object or null if not found
|
|
261
|
+
*/
|
|
262
|
+
function findCodeBlockByUUID(messageText, blockUUID) {
|
|
263
|
+
if (!messageText || !blockUUID) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
// Use the extractor function now part of this module
|
|
267
|
+
const codeBlocks = extractCodeBlocksWithUUIDs(messageText);
|
|
268
|
+
return codeBlocks.find(block => block.blockUUID === blockUUID) || null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = {
|
|
272
|
+
findAllCodeFences,
|
|
273
|
+
matchFencesAndExtractBlocks,
|
|
274
|
+
extractCodeBlocksWithUUIDs,
|
|
275
|
+
findCodeBlockByUUID
|
|
276
|
+
};
|
|
277
|
+
|