|
19 | 19 | import yaml |
20 | 20 | import re |
21 | 21 | import os |
| 22 | +from datetime import datetime |
| 23 | +from typing import Any, Dict, List |
22 | 24 |
|
23 | 25 | # ----------------------- |
24 | 26 | # Load YAML data |
|
27 | 29 | with open('data/plugins-and-themes.yml', 'r') as f: |
28 | 30 | data = yaml.safe_load(f) |
29 | 31 |
|
| 32 | +# ----------------------- |
| 33 | +# Helpers |
| 34 | +# ----------------------- |
| 35 | + |
| 36 | +def _parse_date_safe(s: str | None) -> datetime: |
| 37 | + """ |
| 38 | + Parse ISO-ish date strings for added_at/updated_at. |
| 39 | + Falls back to datetime.min if missing/invalid so items without dates |
| 40 | + naturally sink to the bottom when sorting by "most recent". |
| 41 | + """ |
| 42 | + if not s or not isinstance(s, str): |
| 43 | + return datetime.min |
| 44 | + s = s.strip() |
| 45 | + if not s: |
| 46 | + return datetime.min |
| 47 | + |
| 48 | + # Try a few common formats first |
| 49 | + for fmt in ( |
| 50 | + "%Y-%m-%d", |
| 51 | + "%Y-%m-%dT%H:%M:%S%z", |
| 52 | + "%Y-%m-%dT%H:%M:%S", |
| 53 | + "%Y-%m-%dT%H:%M:%S.%fZ", |
| 54 | + ): |
| 55 | + try: |
| 56 | + return datetime.strptime(s, fmt) |
| 57 | + except ValueError: |
| 58 | + continue |
| 59 | + |
| 60 | + # Fallback: fromisoformat for other variants |
| 61 | + try: |
| 62 | + return datetime.fromisoformat(s) |
| 63 | + except Exception: |
| 64 | + return datetime.min |
| 65 | + |
| 66 | +def _recent_plugins(entries: List[Dict[str, Any]], limit: int = 6) -> List[Dict[str, Any]]: |
| 67 | + """ |
| 68 | + Return up to `limit` most recently added plugins by `added_at` (desc). |
| 69 | + If `added_at` is missing/invalid, that entry will sort as very old. |
| 70 | + """ |
| 71 | + plugins_only = [e for e in entries if isinstance(e, dict)] |
| 72 | + plugins_only.sort( |
| 73 | + key=lambda e: _parse_date_safe(e.get("added_at")), |
| 74 | + reverse=True, |
| 75 | + ) |
| 76 | + return plugins_only[:limit] |
| 77 | + |
| 78 | +def _escape_inline(text: Any) -> str: |
| 79 | + """ |
| 80 | + Light inline escaping/sanitizing for markdown text. |
| 81 | + Not for tables – just make sure we have a clean string. |
| 82 | + """ |
| 83 | + if not isinstance(text, str): |
| 84 | + return "" |
| 85 | + return text.strip() |
| 86 | + |
| 87 | +def generate_recent_plugins_md(entries: List[Dict[str, Any]]) -> str: |
| 88 | + """ |
| 89 | + Build the 'Recently Added Plugins' section content for PLUGINS.md. |
| 90 | +
|
| 91 | + Injected between: |
| 92 | + <!--- Recently Added Plugins Start --> |
| 93 | + <!--- Recently Added Plugins End --> |
| 94 | + """ |
| 95 | + if not entries: |
| 96 | + return "No recently added plugins found.\n" |
| 97 | + |
| 98 | + lines: List[str] = [] |
| 99 | + lines.append("> These are the six most recently added plugins (based on `added_at`).") |
| 100 | + lines.append("") |
| 101 | + |
| 102 | + for idx, entry in enumerate(entries, start=1): |
| 103 | + name = _escape_inline(entry.get("name", "Unknown")) |
| 104 | + repo = entry.get("repo") |
| 105 | + desc = _escape_inline(entry.get("description", "")) |
| 106 | + mc = _escape_inline(entry.get("mc_versions", "")) |
| 107 | + |
| 108 | + creator_obj = entry.get("creator") or {} |
| 109 | + creator_name = _escape_inline(creator_obj.get("name", "Unknown")) |
| 110 | + creator_url = creator_obj.get("url") |
| 111 | + added_at = _escape_inline(entry.get("added_at", "")) |
| 112 | + |
| 113 | + if repo: |
| 114 | + name_md = f"[{name}](https://github.com/{repo})" |
| 115 | + else: |
| 116 | + name_md = name |
| 117 | + |
| 118 | + if creator_url: |
| 119 | + creator_md = f"[{creator_name}]({creator_url})" |
| 120 | + else: |
| 121 | + creator_md = creator_name |
| 122 | + |
| 123 | + meta_bits = [] |
| 124 | + if mc: |
| 125 | + meta_bits.append(f"`MC: {mc}`") |
| 126 | + if creator_md: |
| 127 | + meta_bits.append(f"by {creator_md}") |
| 128 | + if added_at: |
| 129 | + meta_bits.append(f"added `{added_at}`") |
| 130 | + |
| 131 | + meta_line = " · ".join(meta_bits) if meta_bits else "" |
| 132 | + desc_part = f" – {desc}" if desc else "" |
| 133 | + |
| 134 | + lines.append(f"{idx}. **{name_md}**{desc_part}") |
| 135 | + if meta_line: |
| 136 | + lines.append(f" {meta_line}") |
| 137 | + lines.append("") # blank line between items |
| 138 | + |
| 139 | + return "\n".join(lines).rstrip() + "\n" |
| 140 | + |
30 | 141 | # ----------------------- |
31 | 142 | # Markdown generator for each plugin/theme entry |
32 | 143 | # ----------------------- |
@@ -126,6 +237,17 @@ def generate_entry_md(entry, is_plugin=True, index=0): |
126 | 237 | flags=re.DOTALL |
127 | 238 | ) |
128 | 239 |
|
| 240 | +# Inject "Recently Added Plugins" section (top 6 by added_at) |
| 241 | +recent_plugins = _recent_plugins(data.get("plugins", []), limit=6) |
| 242 | +recent_md_block = generate_recent_plugins_md(recent_plugins) |
| 243 | + |
| 244 | +plugins_content = re.sub( |
| 245 | + r'<!--- Recently Added Plugins Start -->.*<!--- Recently Added Plugins End -->', |
| 246 | + f'<!--- Recently Added Plugins Start -->\n{recent_md_block}<!--- Recently Added Plugins End -->', |
| 247 | + plugins_content, |
| 248 | + flags=re.DOTALL |
| 249 | +) |
| 250 | + |
129 | 251 | with open('PLUGINS.md', 'w') as f: |
130 | 252 | f.write(plugins_content) |
131 | 253 |
|
|
0 commit comments