""" Frontmatter rules engine """ import json import re from pathlib import Path from typing import List, Tuple class RulesEngine: def __init__(self, rules_dir: Path): self.rules_dir = rules_dir self.rules = self._load_rules() def _load_rules(self) -> List[dict]: """Load all rule files""" if not self.rules_dir.exists(): self.rules_dir.mkdir(parents=True) self._create_default_rules() rules = [] for rule_file in self.rules_dir.glob('*.json'): with open(rule_file, 'r', encoding='utf-8') as f: rules.append(json.load(f)) return rules def _create_default_rules(self): """Create default rule files""" # Tags normalization rule tags_rule = { "name": "tags_normalization", "description": "Normalize tags to lowercase with underscores", "type": "tags" } tag_to_tags_rule = { "name": "tag_to_tags", "description": "Rename 'tag' key to 'tags'", "type": "rename" } lowercase_keys_rule = { "name": "lowercase_keys", "description": "All frontmatter keys must be lowercase", "type": "keys" } summary_to_description_rule = { "name": "summary_to_description", "description": "Rename 'summary' key to 'description'", "type": "rename" } with open(self.rules_dir / 'tags_normalization.json', 'w', encoding='utf-8') as f: json.dump(tags_rule, f, indent=2) with open(self.rules_dir / 'tag_to_tags.json', 'w', encoding='utf-8') as f: json.dump(tag_to_tags_rule, f, indent=2) with open(self.rules_dir / 'lowercase_keys.json', 'w', encoding='utf-8') as f: json.dump(lowercase_keys_rule, f, indent=2) with open(self.rules_dir / 'summary_to_description.json', 'w', encoding='utf-8') as f: json.dump(summary_to_description_rule, f, indent=2) def apply_rules(self, frontmatter: dict) -> Tuple[dict, bool]: """Apply all rules to frontmatter, return (modified_fm, is_conformant)""" if not frontmatter: return frontmatter, True modified = dict(frontmatter) violations = [] # Rule: lowercase keys (run first so we can check other rules) key_changes = {} for key in list(modified.keys()): lower_key = key.lower() if lower_key != key: key_changes[key] = lower_key violations.append('lowercase_keys') for old_key, new_key in key_changes.items(): modified[new_key] = modified.pop(old_key) # Rule: tag -> tags (after lowercase conversion) if 'tag' in modified: modified['tags'] = modified.pop('tag') violations.append('tag_to_tags') # Rule: summary -> description (after lowercase conversion) if 'summary' in modified: modified['description'] = modified.pop('summary') violations.append('summary_to_description') # Rule: normalize tags if 'tags' in modified: original_tags = modified['tags'] # Convert to list if comma-separated string if isinstance(original_tags, str): tags_list = [t.strip() for t in original_tags.split(',')] violations.append('tags_format') else: tags_list = original_tags if isinstance(original_tags, list) else [str(original_tags)] # Normalize each tag normalized_tags = [] for tag in tags_list: tag_str = str(tag) # Replace spaces and hyphens with underscores normalized = tag_str.replace(' ', '_').replace('-', '_') # Convert to lowercase normalized = normalized.lower() # Remove invalid characters (keep only alphanumeric and underscore) normalized = re.sub(r'[^a-z0-9_]', '', normalized) if normalized and normalized != tag_str: violations.append('tags_normalized') if normalized: normalized_tags.append(normalized) modified['tags'] = normalized_tags is_conformant = len(violations) == 0 return modified, is_conformant