Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 50 additions & 11 deletions src/core/output/outputGenerate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,30 +53,67 @@ const getCompiledTemplate = (style: string): Handlebars.TemplateDelegate => {
};

const calculateMarkdownDelimiter = (files: ReadonlyArray<ProcessedFile>): string => {
const maxBackticks = files
.flatMap((file) => file.content.match(/`+/g) ?? [])
.reduce((max, match) => Math.max(max, match.length), 0);
return '`'.repeat(Math.max(3, maxBackticks + 1));
// Single-pass character scan: avoid regex/flatMap overhead and intermediate array allocation
let maxLen = 0;
for (const file of files) {
let currentLen = 0;
for (let i = 0; i < file.content.length; i++) {
if (file.content[i] === '`') {
currentLen++;
if (currentLen > maxLen) {
maxLen = currentLen;
}
} else {
currentLen = 0;
}
}
}
return '`'.repeat(Math.max(3, maxLen + 1));
};

const countNewlines = (content: string): number => {
let count = 0;
let pos = content.indexOf('\n');
while (pos !== -1) {
count++;
pos = content.indexOf('\n', pos + 1);
}
return count;
};

const calculateFileLineCounts = (processedFiles: ProcessedFile[]): Record<string, number> => {
const lineCounts: Record<string, number> = {};
for (const file of processedFiles) {
// Count lines: empty files have 0 lines, otherwise count newlines + 1
// (unless the content ends with a newline, in which case the last "line" is empty)
const content = file.content;
if (content.length === 0) {
lineCounts[file.path] = 0;
} else {
// Count actual lines (text editor style: number of \n + 1, but trailing \n doesn't add extra line)
const newlineCount = (content.match(/\n/g) || []).length;
const newlineCount = countNewlines(content);
lineCounts[file.path] = content.endsWith('\n') ? newlineCount : newlineCount + 1;
}
}
return lineCounts;
};

export const createRenderContext = (outputGeneratorContext: OutputGeneratorContext): RenderContext => {
export interface CreateRenderContextOptions {
// When true, compute all fields regardless of output style (needed by skill path)
forceAll?: boolean;
}

export const createRenderContext = (
outputGeneratorContext: OutputGeneratorContext,
options: CreateRenderContextOptions = {},
): RenderContext => {
const style = outputGeneratorContext.config.output.style;

// fileLineCounts: not referenced by any Handlebars template or parsable output generator.
// Only the skill path (packSkill.ts) uses it, so skip the full-content scan for regular output.
const needsLineCounts = !!options.forceAll;

// markdownCodeBlockDelimiter: only used by the markdown template and skill generators.
// For XML, plain, JSON, and parsable-XML output, skip the full-content regex scan.
const needsMarkdownDelimiter = options.forceAll || style === 'markdown';

return {
generationHeader: generateHeader(outputGeneratorContext.config, outputGeneratorContext.generationDate),
summaryPurpose: generateSummaryPurpose(outputGeneratorContext.config),
Expand All @@ -90,12 +127,14 @@ export const createRenderContext = (outputGeneratorContext: OutputGeneratorConte
instruction: outputGeneratorContext.instruction,
treeString: outputGeneratorContext.treeString,
processedFiles: outputGeneratorContext.processedFiles,
fileLineCounts: calculateFileLineCounts(outputGeneratorContext.processedFiles),
fileLineCounts: needsLineCounts ? calculateFileLineCounts(outputGeneratorContext.processedFiles) : {},
fileSummaryEnabled: outputGeneratorContext.config.output.fileSummary,
directoryStructureEnabled: outputGeneratorContext.config.output.directoryStructure,
filesEnabled: outputGeneratorContext.config.output.files,
escapeFileContent: outputGeneratorContext.config.output.parsableStyle,
markdownCodeBlockDelimiter: calculateMarkdownDelimiter(outputGeneratorContext.processedFiles),
markdownCodeBlockDelimiter: needsMarkdownDelimiter
? calculateMarkdownDelimiter(outputGeneratorContext.processedFiles)
: '```',
gitDiffEnabled: outputGeneratorContext.config.output.git?.includeDiffs,
gitDiffWorkTree: outputGeneratorContext.gitDiffResult?.workTreeDiffContent,
gitDiffStaged: outputGeneratorContext.gitDiffResult?.stagedDiffContent,
Expand Down
2 changes: 1 addition & 1 deletion src/core/skill/packSkill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const generateSkillReferences = async (
gitDiffResult,
gitLogResult,
);
const renderContext = createRenderContext(outputGeneratorContext);
const renderContext = createRenderContext(outputGeneratorContext, { forceAll: true });

// Calculate statistics
const statistics = calculateStatistics(sortedProcessedFiles, renderContext.fileLineCounts);
Expand Down
Loading