Skip to content
Merged
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
3 changes: 2 additions & 1 deletion generate-symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@

import { createSymbolMapGenerator } from './src/lib/utils/symbol-generation.js';
import { createLogger } from './src/lib/utils/logger.js';
import { getVersion } from './src/lib/utils/version.js';

const logger = createLogger('generate-symbols');

const generator = createSymbolMapGenerator({
sourcePatterns: ['src/lib/**/*.ts'],
excludePatterns: ['**/*.test.ts', '**/*.spec.ts', '**/node_modules/**', '**/dist/**'],
cacheDir: '.dev/tmp',
cacheVersion: '1.0.0',
cacheVersion: getVersion(),
outputPath: 'docs/.generated/symbol-map.json',
baseDir: process.cwd(),
});
Expand Down
2 changes: 1 addition & 1 deletion packages/docs-engine-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ program
.option('-o, --output <path>', 'Output path for symbol map', 'docs/symbol-map.json')
.option('-e, --exclude <patterns...>', 'Patterns to exclude')
.option('--cache-dir <path>', 'Cache directory', '.cache')
.option('--cache-version <version>', 'Cache version', '1.0')
.option('--cache-version <version>', 'Cache version', getVersion())
.option('-w, --watch', 'Watch files and regenerate on changes')
.option('-b, --benchmark', 'Run performance benchmark')
.option('--debounce <ms>', 'Debounce delay for watch mode in ms', '500')
Expand Down
152 changes: 47 additions & 105 deletions src/lib/generators/generic-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,26 +138,21 @@ const readFileAsync = promisify(readFile);
* Generic documentation generator
*/
export class GenericGenerator {
constructor(private config: GeneratorConfig) {}
readonly #config: GeneratorConfig;

constructor(config: GeneratorConfig) {
this.#config = config;
}

/**
* Generate documentation
*/
async generate(): Promise<GeneratorResult> {
// 1. Parse source file
const items = await this.parse();

// 2. Categorize items
const categorized = this.categorize(items);

// 3. Enrich with metadata
const enriched = this.enrich(categorized);

// 4. Generate markdown
const markdown = this.generateMarkdown(enriched);

// 5. Calculate stats
const stats = this.calculateStats(enriched);
const items = await this.#parse();
const categorized = this.#categorize(items);
const enriched = this.#enrich(categorized);
const markdown = this.#generateMarkdown(enriched);
const stats = this.#calculateStats(enriched);

return {
markdown,
Expand All @@ -166,55 +161,42 @@ export class GenericGenerator {
};
}

/**
* Parse source file based on parser configuration
*/
private async parse(): Promise<ParsedItem[]> {
const content = await readFileAsync(this.config.input, 'utf-8');
const parser = this.config.parser;
async #parse(): Promise<ParsedItem[]> {
const content = await readFileAsync(this.#config.input, 'utf-8');
const parser = this.#config.parser;

switch (parser.type) {
case 'json':
return parseJSON(content, parser.path);

case 'env':
return parseEnv(content, parser.categoryPrefix);

case 'sql':
return parseSQL(content, parser.tablePattern);

case 'grep':
return parseGrep(parser.command, parser.extractPattern);

case 'custom':
return parser.parse(content, this.config);

return parser.parse(content, this.#config);
default:
throw new Error(`Unknown parser type: ${(parser as { type?: string }).type}`);
}
}

/**
* Categorize items based on rules
*/
private categorize(items: ParsedItem[]): Map<string, ParsedItem[]> {
#categorize(items: ParsedItem[]): Map<string, ParsedItem[]> {
const categorized = new Map<string, ParsedItem[]>();

// Initialize categories
for (const rule of this.config.categories) {
for (const rule of this.#config.categories) {
categorized.set(rule.name, []);
}
categorized.set('__uncategorized__', []);

// Apply rules
for (const item of items) {
let assigned = false;

for (const rule of this.config.categories) {
if (this.matchesRule(item, rule)) {
for (const rule of this.#config.categories) {
if (this.#matchesRule(item, rule)) {
categorized.get(rule.name)?.push({ ...item, category: rule.name });
assigned = true;
break; // First match wins
break;
}
}

Expand All @@ -223,59 +205,44 @@ export class GenericGenerator {
}
}

// Remove empty categories
for (const [name, items] of categorized) {
if (items.length === 0) {
for (const [name, categoryItems] of categorized) {
if (categoryItems.length === 0) {
categorized.delete(name);
}
}

return categorized;
}

/**
* Check if item matches category rule
*/
private matchesRule(item: ParsedItem, rule: CategoryRule): boolean {
#matchesRule(item: ParsedItem, rule: CategoryRule): boolean {
if (typeof rule.match === 'function') {
return rule.match(item);
}

// Regex pattern
const pattern = new RegExp(rule.match);
const testValue = String(item.name ?? item.value ?? item.key ?? item);
return pattern.test(testValue);
}

/**
* Enrich items with metadata
*/
private enrich(categorized: Map<string, ParsedItem[]>): Map<string, ParsedItem[]> {
if (!this.config.enrichments && !this.config.descriptions) {
#enrich(categorized: Map<string, ParsedItem[]>): Map<string, ParsedItem[]> {
if (!this.#config.enrichments && !this.#config.descriptions) {
return categorized;
}

const enriched = new Map<string, ParsedItem[]>();

for (const [category, items] of categorized) {
for (const [category, categoryItems] of categorized) {
enriched.set(
category,
items.map((item) => this.enrichItem(item))
categoryItems.map((item) => this.#enrichItem(item))
);
}

return enriched;
}

/**
* Enrich single item
*/
private enrichItem(item: ParsedItem): ParsedItem {
#enrichItem(item: ParsedItem): ParsedItem {
const enriched = { ...item };

// Apply enrichment rules
if (this.config.enrichments) {
for (const rule of this.config.enrichments) {
if (this.#config.enrichments) {
for (const rule of this.#config.enrichments) {
if (typeof rule.value === 'function') {
enriched[rule.field] = rule.value(item);
} else {
Expand All @@ -285,93 +252,79 @@ export class GenericGenerator {
}
}

// Apply descriptions
if (this.config.descriptions) {
if (this.#config.descriptions) {
const key = String(item.name ?? item.key ?? item.value ?? '');
if (key && this.config.descriptions[key]) {
enriched.description = this.config.descriptions[key];
if (key && this.#config.descriptions[key]) {
enriched.description = this.#config.descriptions[key];
}
}

return enriched;
}

/**
* Generate markdown from categorized items
*/
private generateMarkdown(categorized: Map<string, ParsedItem[]>): string {
#generateMarkdown(categorized: Map<string, ParsedItem[]>): string {
const sections: string[] = [];
const template = this.config.template;
const template = this.#config.template;

// Title
sections.push(`# ${template.title}\n`);

// Source header
if (template.source) {
sections.push(`> **Source**: ${template.source}`);
sections.push(`> **Generated**: ${new Date().toISOString()}\n`);
}

// Overview
if (template.overview) {
sections.push('## Overview\n');
if (typeof template.overview === 'function') {
const stats = this.calculateStats(categorized);
const stats = this.#calculateStats(categorized);
sections.push(template.overview(stats) + '\n');
} else {
sections.push(template.overview + '\n');
}
}

// Statistics
if (template.showStats) {
const stats = this.calculateStats(categorized);
const stats = this.#calculateStats(categorized);
sections.push('### Statistics\n');
sections.push(`- **Total items**: ${stats.totalItems}`);
sections.push(`- **Categories**: ${stats.categoryCount}\n`);
}

// Table of contents
if (template.showTOC) {
sections.push('## Categories\n');
for (const [category, items] of categorized) {
for (const [category, categoryItems] of categorized) {
if (category === '__uncategorized__') continue;
const anchor = category.toLowerCase().replace(/[^a-z0-9]+/g, '-');
sections.push(`- [${category}](#${anchor}) (${items.length} items)`);
sections.push(`- [${category}](#${anchor}) (${categoryItems.length} items)`);
}
sections.push('');
}

// Category sections
for (const [category, items] of categorized) {
for (const [category, categoryItems] of categorized) {
if (category === '__uncategorized__') continue;

sections.push(`## ${category}\n`);

// Generate table
const headers = template.columns.map((col) => col.header);
const rows = items.map((item) =>
const rows = categoryItems.map((item) =>
template.columns.map((col) => {
let value: unknown;
if (typeof col.field === 'function') {
value = col.field(item);
} else {
value = item[col.field];
}

if (col.format) {
value = col.format(value);
}

return String(value);
})
);

sections.push(this.generateTable(headers, rows));
sections.push(this.#generateTable(headers, rows));
sections.push('');
}

// Footer
if (template.footer) {
sections.push('---\n');
if (Array.isArray(template.footer)) {
Expand All @@ -384,43 +337,32 @@ export class GenericGenerator {
return sections.join('\n');
}

/**
* Generate markdown table
*/
private generateTable(headers: string[], rows: string[][]): string {
#generateTable(headers: string[], rows: string[][]): string {
const lines: string[] = [];

// Header
lines.push('| ' + headers.join(' | ') + ' |');
lines.push('| ' + headers.map(() => '---').join(' | ') + ' |');

// Rows
for (const row of rows) {
lines.push('| ' + row.join(' | ') + ' |');
}

return lines.join('\n');
}

/**
* Calculate statistics
*/
private calculateStats(categorized: Map<string, ParsedItem[]>): GeneratorStats {
#calculateStats(categorized: Map<string, ParsedItem[]>): GeneratorStats {
const stats: GeneratorStats = {
totalItems: 0,
categoryCount: 0,
itemsByCategory: {},
uncategorized: 0,
};

for (const [category, items] of categorized) {
for (const [category, categoryItems] of categorized) {
if (category === '__uncategorized__') {
stats.uncategorized = items.length;
stats.uncategorized = categoryItems.length;
} else {
stats.categoryCount++;
stats.itemsByCategory[category] = items.length;
stats.itemsByCategory[category] = categoryItems.length;
}
stats.totalItems += items.length;
stats.totalItems += categoryItems.length;
}

return stats;
Expand Down
Loading
Loading