diff --git a/src/services/rules-builder/markdown-builders/index.ts b/src/services/rules-builder/markdown-builders/index.ts new file mode 100644 index 0000000..d79bf86 --- /dev/null +++ b/src/services/rules-builder/markdown-builders/index.ts @@ -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; + librariesByStack: Record; + 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, +}); diff --git a/src/services/rules-builder/rules-generation-strategies/MultiFileRulesStrategy.ts b/src/services/rules-builder/rules-generation-strategies/MultiFileRulesStrategy.ts index 124b632..18a6266 100644 --- a/src/services/rules-builder/rules-generation-strategies/MultiFileRulesStrategy.ts +++ b/src/services/rules-builder/rules-generation-strategies/MultiFileRulesStrategy.ts @@ -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 @@ -15,72 +21,46 @@ export class MultiFileRulesStrategy implements RulesGenerationStrategy { stacksByLayer: Record, librariesByStack: Record, ): 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, - ); } diff --git a/src/services/rules-builder/rules-generation-strategies/SingleFileRulesStrategy.ts b/src/services/rules-builder/rules-generation-strategies/SingleFileRulesStrategy.ts index 467c9e7..d8a66ca 100644 --- a/src/services/rules-builder/rules-generation-strategies/SingleFileRulesStrategy.ts +++ b/src/services/rules-builder/rules-generation-strategies/SingleFileRulesStrategy.ts @@ -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 @@ -14,15 +20,13 @@ export class SingleFileRulesStrategy implements RulesGenerationStrategy { stacksByLayer: Record, librariesByStack: Record, ): 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 }]; } @@ -35,35 +39,34 @@ export class SingleFileRulesStrategy implements RulesGenerationStrategy { librariesByStack: Record, ): 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;