"""Frontmatter parser for process files.
Parses YAML frontmatter from markdown files to extract metadata.
"""
from __future__ import annotations
import re
from typing import TYPE_CHECKING, Any
import frontmatter
from sso_mcp_server import get_logger
if TYPE_CHECKING:
from pathlib import Path
_logger = get_logger("process_parser")
def normalize_name(name: str) -> str:
"""Normalize a process name for case-insensitive matching.
Removes special characters, replaces spaces with hyphens, and lowercases.
Args:
name: Process name to normalize.
Returns:
Normalized name suitable for matching.
"""
# Convert to lowercase
normalized = name.lower()
# Replace spaces with hyphens
normalized = normalized.replace(" ", "-")
# Remove special characters (keep alphanumeric and hyphens)
normalized = re.sub(r"[^a-z0-9\-]", "", normalized)
# Collapse multiple hyphens
normalized = re.sub(r"-+", "-", normalized)
# Strip leading/trailing hyphens
normalized = normalized.strip("-")
return normalized
def parse_process_file(file_path: Path) -> dict[str, Any] | None:
"""Parse a process markdown file with YAML frontmatter.
Extracts name, description, and content from the file.
Files without frontmatter use filename as name with no description.
Args:
file_path: Path to the markdown file.
Returns:
Dictionary with name, description, content, path, and normalized_name.
Returns None if file doesn't exist or can't be parsed.
"""
if not file_path.exists():
_logger.debug("file_not_found", path=str(file_path))
return None
try:
post = frontmatter.load(file_path)
# Extract metadata from frontmatter
name = post.metadata.get("name", file_path.stem)
description = post.metadata.get("description", "")
# Content is the body without frontmatter
content = post.content.strip()
result = {
"name": name,
"description": description,
"content": content,
"path": file_path,
"normalized_name": normalize_name(name),
}
_logger.debug(
"process_parsed",
path=str(file_path),
name=name,
has_description=bool(description),
)
return result
except Exception as e:
_logger.warning("parse_error", path=str(file_path), error=str(e))
# Return basic info using filename (edge case: malformed frontmatter)
name = file_path.stem
return {
"name": name,
"description": "",
"content": "",
"path": file_path,
"normalized_name": normalize_name(name),
}