128 lines
4.5 KiB
Python
128 lines
4.5 KiB
Python
"""
|
|
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
|