Skip to content
Open
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
102 changes: 102 additions & 0 deletions src/services/rules-builder/markdown-builders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { Layer, Library, Stack } from '../../../data/dictionaries.ts';
import { getRulesForLibrary } from '../../../data/rules.ts';
import { slugify } from '../../../utils/slugify.ts';

export const createProjectMarkdown = (projectName: string, projectDescription: string): string =>
`# AI Rules for ${projectName}\n\n${projectDescription}\n\n`;

export const createEmptyStateMarkdown = (): string =>
`---\n\n👈 Use the Rule Builder on the left or drop dependency file here`;

export const getProjectMetadata = () => ({
label: 'Project',
fileName: 'project.mdc' as const,
});

export interface LibrarySectionConfig {
layer: string;
stack: string;
library: string;
includeLayerHeader?: boolean;
includeStackHeader?: boolean;
}

export const renderLibrarySection = ({
layer,
stack,
library,
includeLayerHeader = false,
includeStackHeader = false,
}: LibrarySectionConfig): string => {
let markdown = '';

if (includeLayerHeader) {
markdown += `## ${layer}\n\n`;
}

if (includeStackHeader) {
markdown += `### Guidelines for ${stack}\n\n`;
}

markdown += `#### ${library}\n\n`;

const libraryRules = getRulesForLibrary(library);
if (libraryRules.length > 0) {
libraryRules.forEach((rule) => {
markdown += `- ${rule}\n`;
});
} else {
markdown += `- Use ${library} according to best practices\n`;
}

markdown += '\n';

return markdown;
};

export const renderLibraryRulesContent = (library: Library): string => {
const libraryRules = getRulesForLibrary(library);
return libraryRules.length > 0
? libraryRules.map((rule) => `- ${rule}`).join('\n')
: `- Use ${library} according to best practices`;
};

export interface LayerStackIterator {
stacksByLayer: Record<Layer, Stack[]>;
librariesByStack: Record<Stack, Library[]>;
onLibrary: (
layer: Layer,
stack: Stack,
library: Library,
isFirstInStack: boolean,
isFirstInLayer: boolean,
) => void;
}

export const iterateLayersStacksLibraries = ({
stacksByLayer,
librariesByStack,
onLibrary,
}: LayerStackIterator): void => {
Object.entries(stacksByLayer).forEach(([layer, stacks], layerIndex) => {
stacks.forEach((stack, stackIndex) => {
const libraries = librariesByStack[stack];
if (libraries) {
libraries.forEach((library, libraryIndex) => {
onLibrary(
layer as Layer,
stack,
library,
libraryIndex === 0,
layerIndex === 0 && stackIndex === 0,
);
});
}
});
});
};

export const createLibraryFileMetadata = (layer: string, stack: string, library: string) => ({
label: `${layer} - ${stack} - ${library}`,
fileName: `${slugify(`${layer}-${stack}-${library}`)}.mdc` as const,
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import type { RulesGenerationStrategy } from '../RulesGenerationStrategy.ts';
import { Layer, type Library, Stack } from '../../../data/dictionaries.ts';
import type { RulesContent } from '../RulesBuilderTypes.ts';
import { getRulesForLibrary } from '../../../data/rules';
import { slugify } from '../../../utils/slugify.ts';
import {
createProjectMarkdown,
createEmptyStateMarkdown,
getProjectMetadata,
renderLibrarySection,
iterateLayersStacksLibraries,
createLibraryFileMetadata,
} from '../markdown-builders/index.ts';

/**
* Strategy for multi-file rules generation
Expand All @@ -15,72 +21,46 @@ export class MultiFileRulesStrategy implements RulesGenerationStrategy {
stacksByLayer: Record<Layer, Stack[]>,
librariesByStack: Record<Stack, Library[]>,
): RulesContent[] {
const projectMarkdown = `# AI Rules for ${projectName}\n\n${projectDescription}\n\n`;
const noSelectedLibrariesMarkdown = `---\n\n👈 Use the Rule Builder on the left or drop dependency file here`;
const projectLabel = 'Project',
projectFileName = 'project.mdc';
const projectMarkdown = createProjectMarkdown(projectName, projectDescription);
const { label: projectLabel, fileName: projectFileName } = getProjectMetadata();

const markdowns: RulesContent[] = [];

markdowns.push({ markdown: projectMarkdown, label: projectLabel, fileName: projectFileName });

if (selectedLibraries.length === 0) {
markdowns[0].markdown += noSelectedLibrariesMarkdown;
markdowns[0].markdown += createEmptyStateMarkdown();
return markdowns;
}

Object.entries(stacksByLayer).forEach(([layer, stacks]) => {
stacks.forEach((stack) => {
librariesByStack[stack].forEach((library) => {
markdowns.push(
this.buildRulesContent({
layer,
stack,
library,
libraryRules: getRulesForLibrary(library),
}),
);
});
});
iterateLayersStacksLibraries({
stacksByLayer,
librariesByStack,
onLibrary: (layer, stack, library) => {
markdowns.push(this.buildRulesContent({ layer, stack, library }));
},
});

return markdowns;
}

private buildRulesContent({
libraryRules,
layer,
stack,
library,
}: {
libraryRules: string[];
layer: string;
stack: string;
library: string;
}): RulesContent {
const label = `${layer} - ${stack} - ${library}`;
const fileName: RulesContent['fileName'] = `${slugify(`${layer}-${stack}-${library}`)}.mdc`;
const content =
libraryRules.length > 0
? `${libraryRules.map((rule) => `- ${rule}`).join('\n')}`
: `- Use ${library} according to best practices`;
const markdown = this.renderRuleMarkdown({ content, layer, stack, library });
const { label, fileName } = createLibraryFileMetadata(layer, stack, library);
const markdown = renderLibrarySection({
layer,
stack,
library,
includeLayerHeader: true,
includeStackHeader: true,
});
return { markdown, label, fileName };
}

private renderRuleMarkdown = ({
content,
layer,
stack,
library,
}: {
content: string;
layer: string;
stack: string;
library: string;
}) =>
`## ${layer}\n\n### Guidelines for ${stack}\n\n#### ${library}\n\n{{content}}\n\n`.replace(
'{{content}}',
content,
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { RulesGenerationStrategy } from '../RulesGenerationStrategy.ts';
import { Layer, Library, Stack } from '../../../data/dictionaries.ts';
import type { RulesContent } from '../RulesBuilderTypes.ts';
import { getRulesForLibrary } from '../../../data/rules.ts';
import {
createProjectMarkdown,
createEmptyStateMarkdown,
getProjectMetadata,
renderLibrarySection,
iterateLayersStacksLibraries,
} from '../markdown-builders/index.ts';

/**
* Strategy for single-file rules generation
Expand All @@ -14,15 +20,13 @@ export class SingleFileRulesStrategy implements RulesGenerationStrategy {
stacksByLayer: Record<Layer, Stack[]>,
librariesByStack: Record<Stack, Library[]>,
): RulesContent[] {
const projectMarkdown = `# AI Rules for ${projectName}\n\n${projectDescription}\n\n`;
const noSelectedLibrariesMarkdown = `---\n\n👈 Use the Rule Builder on the left or drop dependency file here`;
const projectLabel = 'Project',
projectFileName = 'project.mdc';
const projectMarkdown = createProjectMarkdown(projectName, projectDescription);
const { label: projectLabel, fileName: projectFileName } = getProjectMetadata();

let markdown = projectMarkdown;

if (selectedLibraries.length === 0) {
markdown += noSelectedLibrariesMarkdown;
markdown += createEmptyStateMarkdown();
return [{ markdown, label: projectLabel, fileName: projectFileName }];
}

Expand All @@ -35,35 +39,34 @@ export class SingleFileRulesStrategy implements RulesGenerationStrategy {
librariesByStack: Record<Stack, Library[]>,
): string {
let markdown = '';
let currentLayer = '';
let currentStack = '';

// Generate content for each layer and its stacks
Object.entries(stacksByLayer).forEach(([layer, stacks]) => {
markdown += `## ${layer}\n\n`;

stacks.forEach((stack) => {
markdown += `### Guidelines for ${stack}\n\n`;

const libraries = librariesByStack[stack];
if (libraries) {
libraries.forEach((library) => {
markdown += `#### ${library}\n\n`;

// Get specific rules for this library
const libraryRules = getRulesForLibrary(library);
if (libraryRules.length > 0) {
libraryRules.forEach((rule) => {
markdown += `- ${rule}\n`;
});
} else {
markdown += `- Use ${library} according to best practices\n`;
}
iterateLayersStacksLibraries({
stacksByLayer,
librariesByStack,
onLibrary: (layer, stack, library) => {
const includeLayerHeader = layer !== currentLayer;
const includeStackHeader = stack !== currentStack;

if (includeLayerHeader) {
currentLayer = layer;
}
if (includeStackHeader) {
currentStack = stack;
if (!includeLayerHeader && currentStack) {
markdown += '\n';
});
}
}

markdown += '\n';
});
markdown += renderLibrarySection({
layer,
stack,
library,
includeLayerHeader,
includeStackHeader,
});
},
});

return markdown;
Expand Down