318 lines
9.3 KiB
Python
318 lines
9.3 KiB
Python
"""
|
|
Feature markdown utilities for project status documentation.
|
|
"""
|
|
|
|
import os
|
|
from typing import Dict, Any, List, Optional
|
|
import re
|
|
from datetime import datetime
|
|
|
|
# Mapping of status to emojis
|
|
STATUS_EMOJIS = {
|
|
"completed": "✅",
|
|
"in_progress": "🔄",
|
|
"planned": "📝",
|
|
"error": "☣️",
|
|
"blocked": "⛔",
|
|
"failed": "❌",
|
|
"warning": "⚠️",
|
|
"unknown": "❓",
|
|
"done": "✅",
|
|
"testing": "🧪",
|
|
"review": "👀",
|
|
"design": "🎨",
|
|
"research": "🔍",
|
|
"deprecated": "🗑️",
|
|
"postponed": "⏳",
|
|
}
|
|
|
|
|
|
def emoji_status(status: str) -> str:
|
|
"""Return emoji for a status."""
|
|
return STATUS_EMOJIS.get(status.lower(), STATUS_EMOJIS["unknown"])
|
|
|
|
|
|
def format_feature_md(feature_name: str, data: dict) -> str:
|
|
"""Format a single feature into Markdown."""
|
|
status = data.get("status", "unknown")
|
|
status_emoji = emoji_status(status)
|
|
|
|
lines = [f"## {feature_name}"]
|
|
lines.append(f"- Status: {status_emoji} {status}")
|
|
|
|
if "description" in data:
|
|
lines.append(f"- Description: {data['description']}")
|
|
|
|
if "date" in data:
|
|
lines.append(f"- Last Update: {data['date']}")
|
|
|
|
if "author" in data:
|
|
lines.append(f"- Updated By: {data['author']}")
|
|
|
|
if "details" in data:
|
|
lines.append(f"- Details: {data['details']}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def build_feature_md(features: dict, title: str = "# Feature Tracking") -> str:
|
|
"""Build the full Markdown document from features dictionary."""
|
|
lines = [title, ""]
|
|
|
|
for feature_name, data in features.items():
|
|
lines.append(format_feature_md(feature_name, data))
|
|
lines.append("") # Add an empty line between features
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def extract_features(
|
|
features_file: str, verbose: bool = False
|
|
) -> Dict[str, Dict[str, Any]]:
|
|
"""Extract all valid features from the file, focusing on descriptions."""
|
|
features = {}
|
|
seen_descriptions = set()
|
|
|
|
if not os.path.exists(features_file):
|
|
if verbose:
|
|
print(f"File not found: {features_file}")
|
|
return features
|
|
|
|
with open(features_file, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
|
|
# First, try to extract features from the specific format
|
|
# This format has ### feature_name followed by status, description, date
|
|
status_blocks = re.findall(
|
|
r"### ([^\n]+)\s+\*\*Status:\*\* ([^\n]+)\s+\*\*Description:\*\* ([^\n]+)\s+\*\*Last Update:\*\* ([^\n]+)",
|
|
content,
|
|
)
|
|
|
|
for feature_key, status_line, desc, date in status_blocks:
|
|
desc = desc.strip()
|
|
date = date.strip()
|
|
|
|
if desc and len(desc) > 2 and desc not in seen_descriptions:
|
|
seen_descriptions.add(desc)
|
|
|
|
# Extract status without emoji
|
|
status_match = re.search(
|
|
r"[^a-zA-Z]*([a-zA-Z_]+)", status_line
|
|
)
|
|
status = (
|
|
status_match.group(1).lower()
|
|
if status_match
|
|
else "planned"
|
|
)
|
|
|
|
# Clean up the feature key
|
|
clean_key = re.sub(
|
|
r"[^a-zA-Z0-9_]", "", feature_key.replace(" ", "_").lower()
|
|
)
|
|
if not clean_key or len(clean_key) < 2:
|
|
# Generate key from description
|
|
words = desc.split()[:3]
|
|
clean_key = "_".join(words).lower()
|
|
clean_key = re.sub(r"[^a-zA-Z0-9_]", "", clean_key)
|
|
|
|
features[clean_key] = {
|
|
"status": status,
|
|
"description": desc,
|
|
"date": date,
|
|
"author": "",
|
|
"details": "",
|
|
}
|
|
|
|
# Also try the format: ### **Status:** status \n **Description:** desc \n **Last Update:** date
|
|
alt_blocks = re.findall(
|
|
r"### \*\*Status:\*\* ([^\n]+)\s+\*\*Description:\*\* ([^\n]+)\s+\*\*Last Update:\*\* ([^\n]+)",
|
|
content,
|
|
)
|
|
|
|
for status_line, desc, date in alt_blocks:
|
|
desc = desc.strip()
|
|
date = date.strip()
|
|
|
|
if desc and len(desc) > 2 and desc not in seen_descriptions:
|
|
seen_descriptions.add(desc)
|
|
|
|
# Extract status without emoji
|
|
status_match = re.search(
|
|
r"[^a-zA-Z]*([a-zA-Z_]+)", status_line
|
|
)
|
|
status = (
|
|
status_match.group(1).lower()
|
|
if status_match
|
|
else "planned"
|
|
)
|
|
|
|
# Generate key from description
|
|
words = desc.split()[:3]
|
|
clean_key = "_".join(words).lower()
|
|
clean_key = re.sub(r"[^a-zA-Z0-9_]", "", clean_key)
|
|
|
|
features[clean_key] = {
|
|
"status": status,
|
|
"description": desc,
|
|
"date": date,
|
|
"author": "",
|
|
"details": "",
|
|
}
|
|
|
|
return features
|
|
|
|
|
|
def enrich_features(
|
|
features: Dict[str, Dict[str, Any]]
|
|
) -> Dict[str, Dict[str, Any]]:
|
|
"""Add additional information to features for display purposes."""
|
|
enriched = {}
|
|
|
|
for key, data in features.items():
|
|
enriched[key] = data.copy()
|
|
status = data.get("status", "unknown")
|
|
enriched[key]["status_emoji"] = emoji_status(status)
|
|
|
|
# Add title-cased key for display
|
|
enriched[key]["display_name"] = key.replace("_", " ").title()
|
|
|
|
return enriched
|
|
|
|
|
|
def generate_features_md(
|
|
features: Dict[str, Dict[str, Any]], project_name: str
|
|
) -> str:
|
|
"""Generate a clean features markdown from the features dictionary."""
|
|
lines = [
|
|
"# Feature Tracking",
|
|
"",
|
|
"## Overview",
|
|
"",
|
|
f"This document tracks the implementation status of all features in the {project_name} project.",
|
|
"",
|
|
"## Feature Status Summary",
|
|
"",
|
|
"| Status | Description |",
|
|
"|--------|-------------|",
|
|
"| ✅ Completed | Features that are fully implemented and tested |",
|
|
"| 🔄 In Progress | Features currently being implemented |",
|
|
"| 📝 Planned | Features planned for future implementation |",
|
|
"",
|
|
"## Features",
|
|
"",
|
|
]
|
|
|
|
# Add each feature
|
|
for feature_key, feature in sorted(features.items()):
|
|
status = feature.get("status", "planned").lower()
|
|
status_emoji = emoji_status(status)
|
|
description = feature.get("description", "")
|
|
date = feature.get("date", "") or datetime.now().strftime("%Y-%m-%d")
|
|
author = feature.get("author", "")
|
|
details = feature.get("details", "")
|
|
|
|
lines.append(f"### {feature_key}")
|
|
lines.append("")
|
|
lines.append(f"**Status:** {status_emoji} {status}")
|
|
lines.append(f"**Description:** {description}")
|
|
lines.append(f"**Last Update:** {date}")
|
|
|
|
if author:
|
|
lines.append(f"**Owner:** {author}")
|
|
|
|
if details:
|
|
lines.append(f"**Details:** {details}")
|
|
|
|
lines.append("")
|
|
|
|
# Add categorized lists
|
|
lines.append("## Feature Categories")
|
|
lines.append("")
|
|
|
|
# Completed features
|
|
lines.append("### Completed Features")
|
|
lines.append("")
|
|
completed_count = 0
|
|
for feature_key, feature in sorted(features.items()):
|
|
if feature.get("status", "").lower() in ["completed", "done"]:
|
|
lines.append(
|
|
f"- **{feature_key}**: {feature.get('description', '')}"
|
|
)
|
|
completed_count += 1
|
|
if completed_count == 0:
|
|
lines.append("*No completed features yet.*")
|
|
lines.append("")
|
|
|
|
# In progress features
|
|
lines.append("### In Progress Features")
|
|
lines.append("")
|
|
in_progress_count = 0
|
|
for feature_key, feature in sorted(features.items()):
|
|
if feature.get("status", "").lower() == "in_progress":
|
|
lines.append(
|
|
f"- **{feature_key}**: {feature.get('description', '')}"
|
|
)
|
|
in_progress_count += 1
|
|
if in_progress_count == 0:
|
|
lines.append("*No features currently in progress.*")
|
|
lines.append("")
|
|
|
|
# Planned features
|
|
lines.append("### Planned Features")
|
|
lines.append("")
|
|
planned_count = 0
|
|
for feature_key, feature in sorted(features.items()):
|
|
if feature.get("status", "").lower() == "planned":
|
|
lines.append(
|
|
f"- **{feature_key}**: {feature.get('description', '')}"
|
|
)
|
|
planned_count += 1
|
|
if planned_count == 0:
|
|
lines.append("*No planned features yet.*")
|
|
lines.append("")
|
|
|
|
# Add CSS styling
|
|
lines.append(
|
|
"""<style>
|
|
.feature-card {
|
|
margin-bottom: 20px;
|
|
padding: 15px;
|
|
border-radius: 5px;
|
|
border-left: 5px solid #ccc;
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.feature-card.completed {
|
|
border-left-color: #28a745;
|
|
background-color: #f0fff0;
|
|
}
|
|
|
|
.feature-card.in_progress {
|
|
border-left-color: #007bff;
|
|
background-color: #f0f8ff;
|
|
}
|
|
|
|
.feature-card.planned {
|
|
border-left-color: #6c757d;
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.feature-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.feature-status {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.feature-date {
|
|
color: #6c757d;
|
|
font-size: 0.9em;
|
|
}
|
|
</style>"""
|
|
)
|
|
|
|
return "\n".join(lines)
|