#!/usr/bin/env python3
"""
Enrich entity pages with Companion Planting sections extracted from
the companion-planting.md concept page.
Reads companion data, builds per-entity companion/anti-companion lists,
and inserts a ## Companion Planting section before the ## Related section
in each entity page.
"""
import sys
import re
from pathlib import Path
from datetime import date
WIKI = Path.home() / 'wiki'
ENTITIES = WIKI / 'entities'
COMPANION_PAGE = WIKI / 'concepts' / 'companion-planting.md'
TODAY = date.today().isoformat()
# ---------------------------------------------------------------------------
# Companion data extracted from companion-planting.md
# ---------------------------------------------------------------------------
# Good companions: entity -> list of (companion, reason)
GOOD_COMPANIONS = {
"scarlett-nantes-carrot": [
("Aromatic herbs, onions", "Repel carrot flies"),
],
"snowball-turnips": [
("Beans, peas, lettuce", "Share soil depth, complementary nutrients"),
],
"bulls-blood-beets": [
("Onions, lettuce, cabbage", "Onions repel pests, beets don't compete"),
],
"sugar-beets": [
("Beans, peas, onions", "Nitrogen fixers + pest repelling"),
],
"butter-crunch-butterhead-lettuce": [
("Onions, cucumbers, carrots", "Shallow roots share space well"),
],
"red-russian-kale": [
("Cabbage, potatoes", "Same family benefits, shared pest management"),
],
"utah-celery": [
("Tomatoes, carrots, lettuce", "Different root zones, shared moisture needs"),
],
"provider-green-bush-beans": [
("Carrots, corn, cucumbers", "Beans fix nitrogen for neighbors"),
],
"sugar-lace-snap-peas": [
("Carrots, radishes, beets", "Early season nitrogen fixers"),
],
"jack-o-lantern-pumpkin": [
("Corn", "Three Sisters method — corn provides structure"),
],
"dark-green-zucchini-squash": [
("Beans, corn, cucumbers", "Squash family shares companions"),
],
"pickling-cucumber": [
("Beans, peas, corn, lettuce", "Beans fix nitrogen, lettuce shares shallow roots"),
],
"roma-tomatoes": [
("Basil, carrots, peppers, lettuce", "Basil repels tomato hornworms"),
],
"california-wonder-sweet-pepper": [
("Tomatoes, basil, carrots", "Shared pest profile, similar needs"),
],
"jalapeno-peppers": [
("Tomatoes, basil, onions", "Shared season, basil deters pests"),
],
"parsley": [
("Tomatoes, asparagus", "Repels asparagus beetles, tomato hornworms"),
],
"large-leaf-basil": [
("Tomatoes, peppers", "Repels hornworms, thrips, mosquitoes"),
],
"coriander-cilantro": [
("Beans, brassicas, cucumbers", "Attracts beneficial insects"),
],
"catnip-herb": [
("General garden", "Attracts pollinators, aids pollination"),
],
"white-bunching-onion": [
("Cabbage family, lettuce, tomatoes", "Repel pests across multiple families"),
],
"long-island-brussels-sprouts": [
("Cabbage family", "Same family benefits, shared pest management"),
],
"yellow-dent-corn": [
("Beans, squash/pumpkin", "Three Sisters method — corn provides structure"),
],
}
# Anti-companions: entity -> list of (avoid, reason)
# Built from "Plants to Avoid Together" section
AVOID_COMPANIONS = {
"roma-tomatoes": [
("Brassicas (kale, brussels sprouts)", "Compete for nutrients, attract shared pests"),
("Corn", "Both heavy feeders, attract earworm/tomato hornworm"),
],
"red-russian-kale": [
("Tomatoes", "Compete for nutrients, attract shared pests"),
],
"long-island-brussels-sprouts": [
("Tomatoes", "Compete for nutrients, attract shared pests"),
],
"provider-green-bush-beans": [
("Onions/Garlic", "Alliums inhibit nitrogen-fixing bacteria on bean roots"),
],
"sugar-lace-snap-peas": [
("Onions/Garlic", "Alliums inhibit nitrogen-fixing bacteria on bean roots"),
],
"dark-green-zucchini-squash": [
("Potatoes", "Same family (nightshades/solanaceae), share blight disease"),
],
"yellow-dent-corn": [
("Tomatoes", "Both heavy feeders, attract earworm/tomato hornworm"),
],
}
# Classic combinations: entity -> list of combination names
CLASSIC_COMBINATIONS = {
"yellow-dent-corn": ["Three Sisters"],
"provider-green-bush-beans": ["Three Sisters"],
"dark-green-zucchini-squash": ["Three Sisters"],
"jack-o-lantern-pumpkin": ["Three Sisters"],
"butter-crunch-butterhead-lettuce": ["Salad Bowl"],
"scarlett-nantes-carrot": ["Salad Bowl", "Mediterranean Bed"],
"large-leaf-basil": ["Salad Bowl", "Mediterranean Bed"],
"coriander-cilantro": ["Salad Bowl"],
"roma-tomatoes": ["Mediterranean Bed"],
"jalapeno-peppers": ["Mediterranean Bed"],
"california-wonder-sweet-pepper": ["Mediterranean Bed"],
"red-russian-kale": ["Cool Season Mix"],
"sugar-lace-snap-peas": ["Cool Season Mix"],
"snowball-turnips": ["Cool Season Mix"],
"white-bunching-onion": ["Cool Season Mix"],
}
# Entity slug -> wiki link name mapping
def wiki_link(slug):
return f"[[{slug}]]"
# ---------------------------------------------------------------------------
# Build companion section text for a given entity
# ---------------------------------------------------------------------------
def build_companion_section(slug):
lines = []
has_content = False
# Good companions
if slug in GOOD_COMPANIONS:
has_content = True
lines.append("## Companion Planting")
lines.append("")
lines.append("### Good Companions")
for companion, reason in GOOD_COMPANIONS[slug]:
# Try to add wikilinks to known companions
linked = add_companion_wikilinks(companion)
lines.append(f"- **{linked}** — {reason}")
lines.append("")
# Anti-companions
if slug in AVOID_COMPANIONS:
if not has_content:
has_content = True
lines.append("## Companion Planting")
lines.append("")
lines.append("### Avoid Growing With")
for avoid, reason in AVOID_COMPANIONS[slug]:
lines.append(f"- **{avoid}** — {reason}")
lines.append("")
# Classic combinations
if slug in CLASSIC_COMBINATIONS:
if not has_content:
has_content = True
lines.append("## Companion Planting")
lines.append("")
combos = ", ".join(CLASSIC_COMBINATIONS[slug])
lines.append(f"### Part of: {combos}")
lines.append(f"- See [[companion-planting]] for full combination details")
lines.append("")
if not has_content:
return None
return "\n".join(lines)
def add_companion_wikilinks(companion_text):
"""Add wikilinks to known entity slugs in companion text."""
# Map common display names to slugs.
# Sort by length descending so longer matches win (e.g. "tomatoes" before "tomato").
name_to_slug = {
"tomatoes": "roma-tomatoes",
"tomato": "roma-tomatoes",
"carrots": "scarlett-nantes-carrot",
"carrot": "scarlett-nantes-carrot",
"onions": "white-bunching-onion",
"onion": "white-bunching-onion",
"peppers": "jalapeno-peppers",
"lettuce": "butter-crunch-butterhead-lettuce",
"beans": "provider-green-bush-beans",
"corn": "yellow-dent-corn",
"cucumbers": "pickling-cucumber",
"cucumber": "pickling-cucumber",
"beets": "sugar-beets",
"cabbage": "long-island-brussels-sprouts",
"kale": "red-russian-kale",
"squash": "dark-green-zucchini-squash",
"pumpkin": "jack-o-lantern-pumpkin",
"basil": "large-leaf-basil",
}
# Process longer names first to avoid partial matches
sorted_names = sorted(name_to_slug.keys(), key=len, reverse=True)
result = companion_text
matched_spans = [] # Track (start, end) of already-matched regions
for name in sorted_names:
pattern = re.compile(r'\b' + re.escape(name) + r'\b', re.IGNORECASE)
for m in pattern.finditer(result):
# Check this span doesn't overlap with an existing match
start, end = m.start(), m.end()
overlaps = any(not (end <= s or start >= e) for s, e in matched_spans)
if not overlaps:
slug = name_to_slug[name]
replacement = f"[[{slug}]]"
result = result[:start] + replacement + result[end:]
matched_spans.append((start, start + len(replacement)))
return result
# ---------------------------------------------------------------------------
# Process entity pages
# ---------------------------------------------------------------------------
def process_entity(filepath):
"""Insert companion section before ## Related, update timestamp."""
slug = filepath.stem
# Skip non-vegetable entities
if slug in ("seedvault-market", "heirloom-seed-starter-pack"):
print(f" ⏭ Skipping {slug} (non-crop entity)")
return
# Skip tree seeds (they have different structure)
tree_seeds = ("chinese-elm", "red-cedar", "southern-yew", "japanese-maple")
if slug in tree_seeds:
print(f" ⏭ Skipping {slug} (tree seed)")
return
content = filepath.read_text()
# Strip any existing companion section (idempotent re-runs)
content = re.sub(
r'## Companion Planting\n.*?(?=\n## )',
'',
content,
flags=re.DOTALL,
)
# Build companion section
companion_section = build_companion_section(slug)
if companion_section is None:
print(f" ⏭ No companion data for {slug}")
return 0
# Insert before ## Related
if "## Related" in content:
new_content = content.replace(
"## Related",
companion_section + "\n## Related",
)
else:
new_content = content + "\n" + companion_section
# Update timestamp
new_content = new_content.replace(
f"updated: {TODAY}",
f"updated: {TODAY}", # Already today, no change needed
)
# Or update if older
if "updated:" in new_content:
new_content = re.sub(
r"updated: \d{4}-\d{2}-\d{2}",
f"updated: {TODAY}",
new_content,
count=1,
)
filepath.write_text(new_content)
print(f" ✓ Updated {slug}")
return 1
def main():
print("Enriching entity pages with companion planting data...")
print()
entity_files = sorted(ENTITIES.glob("*.md"))
updated = 0
skipped = 0
for ef in entity_files:
result = process_entity(ef)
if result == 1:
updated += 1
else:
skipped += 1
print()
print(f"Done: {updated} updated, {skipped} skipped")
if __name__ == "__main__":
main()