Files
festipod/scripts/parse-features.ts
T
Sylvain Duchesne 9843936212 Use grammatically correct French Gherkin keyword "Étant donné que"
Update all 26 feature files to use proper French grammar with the
subordinating conjunction "que" after "Étant donné". Also update the
parser to recognize both "Étant donné que " and "Étant donné qu'"
(elision before vowels), while maintaining backwards compatibility
with the original form.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 19:39:12 +01:00

196 lines
5.7 KiB
TypeScript

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é que ', "Étant donné qu'", 'Étant donné', 'Etant donné que ', "Etant donné qu'", '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);