doctool
Version:
AI-powered documentation validation and management system
277 lines ⢠9.01 kB
JavaScript
import * as readline from 'readline';
/**
* Generates a unified diff between two text contents
*/
export function generateDiff(oldContent, newContent, filePath = '') {
const oldLines = oldContent.split('\n');
const newLines = newContent.split('\n');
const diffLines = computeDiffLines(oldLines, newLines);
return {
filePath,
oldContent,
newContent,
lines: diffLines,
hasChanges: diffLines.some(line => line.type !== 'context')
};
}
/**
* Computes diff lines using a simple LCS algorithm
*/
function computeDiffLines(oldLines, newLines) {
const diffLines = [];
// Simple diff algorithm - for more complex diffs, we could use a library like diff
const lcs = longestCommonSubsequence(oldLines, newLines);
let oldIndex = 0;
let newIndex = 0;
let lcsIndex = 0;
while (oldIndex < oldLines.length || newIndex < newLines.length) {
if (lcsIndex < lcs.length &&
oldIndex < oldLines.length &&
oldLines[oldIndex] === lcs[lcsIndex]) {
// Common line
diffLines.push({
type: 'context',
content: oldLines[oldIndex],
lineNumber: oldIndex + 1
});
oldIndex++;
newIndex++;
lcsIndex++;
}
else if (oldIndex < oldLines.length &&
(lcsIndex >= lcs.length || oldLines[oldIndex] !== lcs[lcsIndex])) {
// Line removed from old
diffLines.push({
type: 'remove',
content: oldLines[oldIndex],
lineNumber: oldIndex + 1
});
oldIndex++;
}
else if (newIndex < newLines.length) {
// Line added in new
diffLines.push({
type: 'add',
content: newLines[newIndex]
});
newIndex++;
}
}
return diffLines;
}
/**
* Simple implementation of Longest Common Subsequence
*/
function longestCommonSubsequence(arr1, arr2) {
const m = arr1.length;
const n = arr2.length;
const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));
// Build LCS table
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (arr1[i - 1] === arr2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
// Reconstruct LCS
const lcs = [];
let i = m, j = n;
while (i > 0 && j > 0) {
if (arr1[i - 1] === arr2[j - 1]) {
lcs.unshift(arr1[i - 1]);
i--;
j--;
}
else if (dp[i - 1][j] > dp[i][j - 1]) {
i--;
}
else {
j--;
}
}
return lcs;
}
/**
* Formats a diff for console display
*/
export function formatDiffForConsole(diff, contextLines = 3) {
if (!diff.hasChanges) {
return `No changes in ${diff.filePath}`;
}
const lines = [];
lines.push(`\nš Changes in ${diff.filePath}:`);
lines.push('ā'.repeat(60));
// Group consecutive changes with context
const groupedLines = groupDiffLines(diff.lines, contextLines);
for (const group of groupedLines) {
for (const line of group) {
let prefix = ' ';
let color = '';
switch (line.type) {
case 'add':
prefix = '+';
color = '\x1b[32m'; // Green
break;
case 'remove':
prefix = '-';
color = '\x1b[31m'; // Red
break;
case 'context':
prefix = ' ';
color = '\x1b[37m'; // White
break;
}
const lineNumber = line.lineNumber ? `${line.lineNumber}`.padStart(4) : ' ';
lines.push(`${color}${prefix}${lineNumber} ${line.content}\x1b[0m`);
}
if (group !== groupedLines[groupedLines.length - 1]) {
lines.push('...');
}
}
return lines.join('\n');
}
/**
* Groups diff lines with context
*/
function groupDiffLines(diffLines, contextLines) {
const groups = [];
let currentGroup = [];
let contextCount = 0;
for (let i = 0; i < diffLines.length; i++) {
const line = diffLines[i];
if (line.type === 'context') {
if (currentGroup.length === 0) {
// Skip leading context if no changes yet
continue;
}
contextCount++;
if (contextCount <= contextLines) {
currentGroup.push(line);
}
else {
// Too much context, start new group
if (currentGroup.length > 0) {
groups.push(currentGroup);
currentGroup = [];
contextCount = 0;
}
}
}
else {
// Change line (add/remove)
currentGroup.push(line);
contextCount = 0;
}
}
if (currentGroup.length > 0) {
groups.push(currentGroup);
}
return groups;
}
/**
* Prompts user for approval with interactive CLI
*/
export function promptUserApproval(message = 'Apply these changes?') {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question(`\n${message} (y/n/q): `, (answer) => {
rl.close();
const normalized = answer.toLowerCase().trim();
if (normalized === 'q' || normalized === 'quit') {
console.log('Exiting...');
process.exit(0);
}
resolve(normalized === 'y' || normalized === 'yes');
});
});
}
/**
* Prompts user with multiple options
*/
export function promptUserChoice(message, choices) {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const choiceText = choices.map((choice, index) => `${index + 1}. ${choice}`).join('\n');
const prompt = `\n${message}\n${choiceText}\nChoice (1-${choices.length}): `;
rl.question(prompt, (answer) => {
rl.close();
const choiceIndex = parseInt(answer.trim()) - 1;
if (choiceIndex >= 0 && choiceIndex < choices.length) {
resolve(choices[choiceIndex]);
}
else {
console.log('Invalid choice, defaulting to first option.');
resolve(choices[0]);
}
});
});
}
export function parseMarkdownSections(content) {
const lines = content.split('\n');
const sections = [];
let currentSection = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
// Close previous section
if (currentSection) {
currentSection.endLine = i - 1;
currentSection.content = lines.slice(currentSection.startLine, i).join('\n');
sections.push(currentSection);
}
// Start new section
currentSection = {
heading: headingMatch[2],
content: '',
startLine: i,
endLine: lines.length - 1,
level: headingMatch[1].length
};
}
}
// Close final section
if (currentSection) {
currentSection.content = lines.slice(currentSection.startLine).join('\n');
sections.push(currentSection);
}
return sections;
}
/**
* Merges two sets of markdown sections intelligently
*/
export function mergeSections(oldSections, newSections) {
const mergedContent = [];
const processedSections = new Set();
// First, add sections that exist in both (prioritizing new content but preserving manual additions)
for (const newSection of newSections) {
const oldSection = oldSections.find(s => s.heading === newSection.heading);
if (oldSection) {
// Section exists in both - use new content but preserve any manual additions
mergedContent.push(newSection.content);
}
else {
// New section - add it
mergedContent.push(newSection.content);
}
processedSections.add(newSection.heading);
}
// Add any old sections that weren't in the new content (manual additions)
for (const oldSection of oldSections) {
if (!processedSections.has(oldSection.heading)) {
mergedContent.push(oldSection.content);
}
}
return mergedContent.join('\n\n');
}
//# sourceMappingURL=diffUtils.js.map