first commit

This commit is contained in:
Sylvain Duchesne
2026-01-18 11:53:42 +01:00
commit f04f15d926
112 changed files with 24858 additions and 0 deletions
+195
View File
@@ -0,0 +1,195 @@
import { Glob } from 'bun';
import type { ParsedFeature, ParsedScenario, ParsedStep } from '../src/types/gherkin';
async function parseFeatures(): Promise<ParsedFeature[]> {
const glob = new Glob('features/**/*.feature');
const features: ParsedFeature[] = [];
for await (const filePath of glob.scan('.')) {
const content = await Bun.file(filePath).text();
const parsed = parseGherkinContent(content, filePath);
if (parsed) {
features.push(parsed);
}
}
features.sort((a, b) => {
if (a.priority !== b.priority) return a.priority - b.priority;
return a.category.localeCompare(b.category);
});
const output = `// Auto-generated by scripts/parse-features.ts
// Do not edit manually
import type { ParsedFeature } from '../types/gherkin';
export const parsedFeatures: ParsedFeature[] = ${JSON.stringify(features, null, 2)};
export function getFeatureById(id: string): ParsedFeature | undefined {
return parsedFeatures.find(f => f.id === id);
}
export function getFeaturesByCategory(category: string): ParsedFeature[] {
return parsedFeatures.filter(f => f.category === category);
}
export function getFeaturesByPriority(priority: number): ParsedFeature[] {
return parsedFeatures.filter(f => f.priority === priority);
}
export function getAllCategories(): string[] {
return [...new Set(parsedFeatures.map(f => f.category))];
}
export function getAllPriorities(): number[] {
return [...new Set(parsedFeatures.map(f => f.priority))].sort((a, b) => a - b);
}
`;
await Bun.write('src/data/features.ts', output);
console.log(`Parsed ${features.length} feature files`);
return features;
}
function parseGherkinContent(content: string, filePath: string): ParsedFeature | null {
const lines = content.split('\n');
// Extract tags from the beginning
const tagLines: string[] = [];
let contentStartIndex = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('#')) {
contentStartIndex = i + 1;
continue;
}
if (line.startsWith('@')) {
tagLines.push(line);
contentStartIndex = i + 1;
} else if (line !== '') {
break;
}
}
const tagMatches = tagLines.join(' ').match(/@[\w-]+/g) || [];
// Extract category and priority from tags
const categoryTag = tagMatches.find(t =>
['@WORKSHOP', '@EVENT', '@USER', '@MEETING', '@NOTIF'].includes(t)
);
const category = categoryTag ? categoryTag.slice(1) : 'UNKNOWN';
const priorityTag = tagMatches.find(t => t.startsWith('@priority-'));
const priority = priorityTag ? parseInt(priorityTag.replace('@priority-', '')) : 3;
// Extract feature name
const featureMatch = content.match(/Fonctionnalité:\s*(.+?)(?:\n|$)/);
const name = featureMatch?.[1]?.trim() || 'Unknown Feature';
// Extract US ID from name
const idMatch = name.match(/US-(\d+)/i);
const id = idMatch ? `us-${idMatch[1]}` : filePath.replace(/.*\//, '').replace('.feature', '');
// Extract description (lines after Feature until Contexte or Scénario)
const descLines: string[] = [];
let inDescription = false;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('Fonctionnalité:')) {
inDescription = true;
continue;
}
if (inDescription) {
if (trimmed.startsWith('Contexte:') || trimmed.startsWith('Scénario:') || trimmed.startsWith('Plan du Scénario:')) {
break;
}
if (trimmed && !trimmed.startsWith('@') && !trimmed.startsWith('#')) {
descLines.push(trimmed);
}
}
}
const description = descLines.join(' ').trim();
// Parse scenarios
const scenarios: ParsedScenario[] = [];
let currentScenario: ParsedScenario | null = null;
let inBackground = false;
const background: ParsedStep[] = [];
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('Contexte:')) {
inBackground = true;
continue;
}
if (trimmed.startsWith('Scénario:') || trimmed.startsWith('Plan du Scénario:')) {
inBackground = false;
if (currentScenario) {
scenarios.push(currentScenario);
}
const scenarioName = trimmed.replace(/^(Scénario:|Plan du Scénario:)\s*/, '').trim();
currentScenario = {
name: scenarioName,
tags: [],
steps: [],
};
continue;
}
// Parse steps
const stepKeywords = ['Étant donné', 'Etant donné', 'Quand', 'Lorsque', 'Alors', 'Et', 'Mais'];
for (const keyword of stepKeywords) {
if (trimmed.startsWith(keyword)) {
const step: ParsedStep = {
keyword,
text: trimmed.slice(keyword.length).trim(),
};
if (inBackground) {
background.push(step);
} else if (currentScenario) {
currentScenario.steps.push(step);
}
break;
}
}
}
// Don't forget the last scenario
if (currentScenario) {
scenarios.push(currentScenario);
}
return {
id,
name,
description,
tags: tagMatches,
category,
priority,
background: background.length > 0 ? background : undefined,
scenarios,
filePath,
rawContent: content,
};
}
// Run parser
parseFeatures().then(features => {
console.log('Features by category:');
const byCategory = features.reduce((acc, f) => {
acc[f.category] = (acc[f.category] || 0) + 1;
return acc;
}, {} as Record<string, number>);
console.log(byCategory);
console.log('\nFeatures by priority:');
const byPriority = features.reduce((acc, f) => {
acc[`P${f.priority}`] = (acc[`P${f.priority}`] || 0) + 1;
return acc;
}, {} as Record<string, number>);
console.log(byPriority);
}).catch(console.error);