feat(script): check H1 emojis against includes/emoji.yaml

This commit is contained in:
nιcнolaѕ wιlde
2026-06-26 23:32:44 -07:00
parent 148f89f8cd
commit ae164fec7d

View File

@ -3,29 +3,294 @@ import os
import re
import sys
import argparse
import subprocess
import yaml
EMOJI_YAML_PATH = "includes/emoji.yaml"
CATEGORY_EMOJIS = {
"beverages": "clinking_glasses",
"cookies-and-bars": "cookie",
"desserts": "cake",
"breads": "bread",
"breakfast": "egg",
"main": "shallow_pan_of_food",
"sides": "salad_bowl",
"sauces-and-dressings": "takeout_box",
"soups-and-stews": "stew",
"lunches": "sandwich",
"salads": "green_salad",
}
HEURISTIC_EMOJIS = {
"corn": "corn",
"succotash": "corn",
"bean": "beans",
"beans": "beans",
"onion": "onion",
"garlic": "garlic",
"tomato": "tomato",
"tomatoes": "tomato",
"pepper": "hot_pepper",
"chili": "hot_pepper",
"chilli": "hot_pepper",
"jalapeño": "hot_pepper",
"basil": "herb",
"parsley": "herb",
"cilantro": "herb",
"rosemary": "herb",
"thyme": "herb",
"oregano": "herb",
"beef": "meat_on_bone",
"chicken": "poultry_leg",
"pork": "pig",
"bacon": "bacon",
"cookie": "cookie",
"cookies": "cookie",
"cake": "cake",
"bread": "bread",
"bun": "bread",
"buns": "bread",
"roll": "bread",
"rolls": "bread",
"soup": "stew",
"stew": "stew",
"salad": "green_salad",
"curry": "curry",
"pie": "pie",
"tart": "pie",
"mousse": "custard",
"pudding": "custard",
"custard": "custard",
"mustard": "takeout_box",
"pickle": "cucumber",
"pickles": "cucumber",
"pickled": "cucumber",
"potato": "potato",
"potatoes": "potato",
"sweet potato": "sweet_potato",
"sweet potatoes": "sweet_potato",
"egg": "egg",
"eggs": "egg",
"cheese": "cheese_wedge",
"cream": "glass_of_milk",
"milk": "glass_of_milk",
"butter": "butter",
"lemon": "lemon",
"lime": "lemon",
"orange": "tangerine",
"apple": "apple",
"apples": "apple",
"banana": "banana",
"bananas": "banana",
"strawberry": "strawberry",
"strawberries": "strawberry",
"blueberry": "blue_circle",
"blueberries": "blue_circle",
"chocolate": "chocolate_bar",
"cocoa": "chocolate_bar",
"peanut": "peanut",
"peanuts": "peanut",
"cashew": "chestnut",
"cashews": "chestnut",
"walnut": "chestnut",
"walnuts": "chestnut",
"almond": "chestnut",
"almonds": "chestnut",
"tofu": "takeout_box",
"rice": "rice",
"noodle": "spaghetti",
"noodles": "spaghetti",
"pasta": "spaghetti",
}
def load_emoji_mappings(filepath):
try:
if not os.path.exists(filepath):
return {}
with open(filepath, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
emoji_data = data.get("emoji", {})
mappings = {}
for item in emoji_data.get("cookware", []):
for emoji_name, terms in item.items():
for term in terms:
mappings[str(term).lower().strip()] = emoji_name
for item in emoji_data.get("ingredients", []):
for emoji_name, terms in item.items():
for term in terms:
mappings[str(term).lower().strip()] = emoji_name
return mappings
except Exception as e:
print(f"Warning: could not load emoji mappings: {e}")
return {}
def check_file(file_path):
"""
Checks if a markdown file's H1 header has an emoji shortcode.
Returns (is_valid, h1_title or error_message)
Returns (is_valid, emoji_shortcode_or_none, h1_title or error_message)
"""
try:
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
if line.startswith("# "):
h1_title = line[2:].strip()
# Matches emoji shortcode e.g., :egg:
if re.match(r"^:[a-zA-Z0-9_+-]+:", h1_title):
return True, h1_title
m = re.match(r"^:([a-zA-Z0-9_+-]+):", h1_title)
if m:
return True, m.group(1), h1_title
else:
return False, h1_title
return False, "No H1 header found"
return False, None, h1_title
return False, None, "No H1 header found"
except Exception as e:
return False, f"Error reading file: {e}"
return False, None, f"Error reading file: {e}"
def suggest_emoji(file_path, mappings):
# Extract title
title = ""
try:
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
if line.startswith("# "):
title = line[2:].strip()
break
except Exception:
pass
if not title:
title = os.path.splitext(os.path.basename(file_path))[0].replace("-", " ")
title_clean = re.sub(r':[a-zA-Z0-9_+-]+:', '', title)
title_words = [w.lower() for w in re.findall(r'\w+', title_clean)]
# 1. Try to find match in title words against our heuristic list
for word in title_words:
if word in HEURISTIC_EMOJIS:
return HEURISTIC_EMOJIS[word]
# 2. Try to find match in title words against emoji.yaml
for word in title_words:
if word in mappings:
return mappings[word]
# 3. Try to find matching ingredient in the file's ingredient section
try:
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
ingredients_section = False
for line in lines:
stripped = line.strip()
if stripped.startswith("## ") and "ingredients" in stripped.lower():
ingredients_section = True
continue
if ingredients_section and stripped.startswith("## "):
ingredients_section = False
if ingredients_section and stripped.startswith("- "):
emoji_match = re.match(r"^-\s+:([a-zA-Z0-9_+-]+):", stripped)
if emoji_match:
return emoji_match.group(1)
ing_line = re.sub(r'^-\s*', '', stripped)
ing_line_lower = ing_line.lower()
# Check against heuristic list
for kw, emoji_name in HEURISTIC_EMOJIS.items():
if kw in ing_line_lower:
return emoji_name
# Check against mappings list, sorting by length descending to get most specific first
matches = []
for term, emoji_name in mappings.items():
if len(term) <= 3 or term in ["fresh", "chopped", "ground", "dried", "water", "salt", "pepper", "oil", "butter"]:
continue
if term in ing_line_lower:
matches.append((term, emoji_name))
if matches:
matches.sort(key=lambda x: len(x[0]), reverse=True)
return matches[0][1]
except Exception:
pass
# 4. Fallback to category
parts = file_path.split(os.sep)
category = parts[1] if len(parts) > 2 else "main"
return CATEGORY_EMOJIS.get(category, "shallow_pan_of_food")
def insert_emoji_mapping(filepath, category, emoji_key, new_term):
with open(filepath, "r", encoding="utf-8") as f:
lines = f.readlines()
category_found = False
inserted = False
for i, line in enumerate(lines):
stripped = line.strip()
if stripped == f"{category}:":
category_found = True
continue
if category_found and (stripped == "ingredients:" or stripped == "cookware:") and line.startswith(" "):
break
if category_found and line.rstrip().endswith(f"- {emoji_key}:"):
indent = " "
if i + 1 < len(lines):
next_line = lines[i + 1]
m_indent = re.match(r'^(\s+)-', next_line)
if m_indent:
indent = m_indent.group(1)
lines.insert(i + 1, f"{indent}- {new_term}\n")
inserted = True
break
if not inserted:
category_index = -1
next_category_index = -1
for i, line in enumerate(lines):
stripped = line.strip()
if stripped == f"{category}:":
category_index = i
elif category_index != -1 and (stripped == "ingredients:" or stripped == "cookware:") and line.startswith(" "):
next_category_index = i
break
insert_pos = next_category_index if next_category_index != -1 else len(lines)
new_group_lines = [
f" - {emoji_key}:\n",
f" - {new_term}\n"
]
for offset, gl in enumerate(new_group_lines):
lines.insert(insert_pos + offset, gl)
with open(filepath, "w", encoding="utf-8") as f:
f.writelines(lines)
def fix_file(file_path, suggested_emoji):
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
lines = content.splitlines()
for idx, line in enumerate(lines):
if line.startswith("# "):
title = line[2:].strip()
new_line = f"# :{suggested_emoji}: {title}"
lines[idx] = new_line
break
with open(file_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
return True
except Exception as e:
print(f"Error fixing {file_path}: {e}")
return False
def main():
parser = argparse.ArgumentParser(description="Check for emoji shortcodes in recipe H1 headers")
parser = argparse.ArgumentParser(description="Check and optionally fix emoji shortcodes in recipe H1 headers")
parser.add_argument("files", nargs="*", help="Optional specific markdown file(s) to check")
parser.add_argument("--fix", action="store_true", help="Automatically insert suggested H1 emoji if missing and register in includes/emoji.yaml")
parser.add_argument("-v", "--verbose", action="store_true", help="Show all checked files with their emojis")
args = parser.parse_args()
docs_dir = "docs"
@ -43,23 +308,96 @@ def main():
if file.endswith(".md") and file not in exclude_files:
files_to_check.append(os.path.join(root, file))
missing_files = []
emoji_mappings = load_emoji_mappings(EMOJI_YAML_PATH)
# Get all uniquely defined emoji shortcodes in emoji.yaml keys
try:
with open(EMOJI_YAML_PATH, "r", encoding="utf-8") as f:
yaml_data = yaml.safe_load(f)
emoji_data = yaml_data.get("emoji", {})
allowed_emojis = set()
for item in emoji_data.get("cookware", []):
allowed_emojis.update(item.keys())
for item in emoji_data.get("ingredients", []):
allowed_emojis.update(item.keys())
except Exception:
allowed_emojis = set()
missing_emoji_files = []
unregistered_emoji_files = []
total_checked = 0
fixed_count = 0
emoji_yaml_modified = False
for file_path in sorted(files_to_check):
total_checked += 1
is_valid, title = check_file(file_path)
if not is_valid:
missing_files.append((file_path, title))
is_valid, emoji, title = check_file(file_path)
if is_valid:
# Check if the emoji used is present in emoji.yaml
if emoji in allowed_emojis:
if args.verbose:
print(f" \u2705 {file_path}: :{emoji}: (Mapped in emoji.yaml)")
else:
if args.fix:
print(f" \u274c {file_path}: H1 emoji :{emoji}: not registered in emoji.yaml. Registering ... ", end="")
clean_title = re.sub(r':[a-zA-Z0-9_+-]+:', '', title).strip()
insert_emoji_mapping(EMOJI_YAML_PATH, "ingredients", emoji, clean_title.lower())
allowed_emojis.add(emoji)
print("Registered!")
fixed_count += 1
emoji_yaml_modified = True
else:
unregistered_emoji_files.append((file_path, title, emoji))
else:
suggestion = suggest_emoji(file_path, emoji_mappings)
if args.fix:
print(f" \u274c {file_path}: Missing emoji. Suggesting :{suggestion}: ... ", end="")
if fix_file(file_path, suggestion):
print("Fixed! ", end="")
# Register the new emoji mapping if not already in emoji.yaml
if suggestion not in allowed_emojis:
print(f"Registering :{suggestion}: in emoji.yaml ... ", end="")
clean_title = re.sub(r':[a-zA-Z0-9_+-]+:', '', title).strip()
insert_emoji_mapping(EMOJI_YAML_PATH, "ingredients", suggestion, clean_title.lower())
allowed_emojis.add(suggestion)
emoji_yaml_modified = True
print("Done!")
fixed_count += 1
else:
print("Failed to fix.")
else:
missing_emoji_files.append((file_path, title, suggestion))
print(f"Checked {total_checked} markdown file(s).")
if missing_files:
print(f"Found {len(missing_files)} file(s) missing an emoji shortcode in H1 header:")
for path, title in missing_files:
print(f"- {path}: {title}")
if emoji_yaml_modified:
# Run sorting
try:
subprocess.run(["task", "emoji-sort"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception:
pass
print(f"\nChecked {total_checked} markdown file(s).")
if args.fix:
print(f"Successfully fixed/registered H1 emojis in {fixed_count} recipe files!")
sys.exit(0)
has_errors = False
if missing_emoji_files:
print(f"\nFound {len(missing_emoji_files)} file(s) missing an emoji shortcode in H1 header:")
for path, title, suggestion in missing_emoji_files:
print(f"- {path}: '{title}' (Suggested: :{suggestion}:)")
has_errors = True
if unregistered_emoji_files:
print(f"\nFound {len(unregistered_emoji_files)} file(s) with H1 emoji not registered in {EMOJI_YAML_PATH}:")
for path, title, emoji in unregistered_emoji_files:
print(f"- {path}: '{title}' (Emoji :{emoji}: is unregistered)")
has_errors = True
if has_errors:
sys.exit(1)
else:
print("All checked recipes contain an emoji shortcode in their H1 header!")
print("All checked recipes contain a registered H1 header emoji!")
sys.exit(0)
if __name__ == "__main__":