@@ -87,69 +87,124 @@ def _escape_inline(text: Any) -> str:
8787 return text .strip ()
8888
8989
90- def _generate_recent_card ( entry : Dict [ str , Any ] ) -> str :
90+ def md_escape ( s : Any ) -> str :
9191 """
92- Generate a single plugin card as a <td> block:
93- - creator avatar
94- - name + 'plugin' label
95- - meta line (MC, creator, added_at)
96- - stars / downloads / updated badges
92+ Match the Top 6 helper: escape < and > so HTML isn't broken.
9793 """
98- name = _escape_inline (entry .get ("name" , "Unknown" ))
99- repo = _escape_inline (entry .get ("repo" , "" ))
94+ s = s or ""
95+ if not isinstance (s , str ):
96+ s = str (s )
97+ return s .replace ("<" , "<" ).replace (">" , ">" )
10098
101- mc = _escape_inline (entry .get ("mc_versions" , "" ))
102- added_at = _escape_inline (entry .get ("added_at" , "" ))
10399
104- creator_obj = entry .get ("creator" ) or {}
105- creator_name = _escape_inline (creator_obj .get ("name" , "" ))
106- creator_url = _escape_inline (creator_obj .get ("url" , "" ))
107- creator_avatar = _escape_inline (creator_obj .get ("avatar" , "" ))
100+ # ---------- Avatar helpers (copied from Top 6 script) ----------
108101
109- parts : List [str ] = []
110- parts .append (' <td valign="top" width="50%">' )
102+ def _is_github_avatar (url : str ) -> bool :
103+ if not url :
104+ return False
105+ return ("avatars.githubusercontent.com" in url ) or bool (
106+ re .search (r"github\.com/.+\.png$" , url )
107+ )
111108
112- # Creator avatar
113- if creator_avatar :
114- parts .append (
115- f' <img src="{ creator_avatar } " alt="{ creator_name } avatar" '
116- f'width="120" height="120"><br>'
117- )
118109
119- # Title line
120- if repo :
121- parts .append (
122- f' <a href="https://github.com/{ repo } "><strong>{ name } </strong></a> '
123- f"<code>plugin</code><br>"
124- )
125- else :
126- parts .append (f" <strong>{ name } </strong> <code>plugin</code><br>" )
127-
128- # Meta line: MC / creator / added date
129- meta_bits : List [str ] = []
130- if mc :
131- meta_bits .append (f"<code>MC: { mc } </code>" )
132- if creator_name :
133- if creator_url :
134- meta_bits .append (f'by <a href="{ creator_url } "><strong>{ creator_name } </strong></a>' )
110+ def _sharp_github_avatar (url : str , px : int = 400 ) -> str :
111+ """Force a crisp GitHub avatar by requesting a larger size, displayed at 100x100."""
112+ if not url :
113+ return url
114+ if "avatars.githubusercontent.com" in url :
115+ return url + (f"&s={ px } " if "?" in url else f"?s={ px } " )
116+ if re .search (r"github\.com/.+\.png$" , url ):
117+ return url + (f"&size={ px } " if "?" in url else f"?size={ px } " )
118+ return url
119+
120+
121+ # ---------- Recently-added cards (Top-6 style) ----------
122+
123+ def _recent_items_for_cards (entries : List [Dict [str , Any ]]) -> List [Dict [str , Any ]]:
124+ """
125+ Normalize YAML plugin entries into the mini dicts that our card
126+ renderer expects, mirroring the Top 6 structure:
127+ - repo, name, desc
128+ - creatorAvatar (hi-res GitHub if possible)
129+ - ownerAvatar fallback (GitHub owner avatar)
130+ - addedUnix: unix timestamp from added_at
131+ """
132+ items : List [Dict [str , Any ]] = []
133+ for e in entries :
134+ repo = (e .get ("repo" ) or "" ).strip ()
135+ if not repo or "/" not in repo :
136+ continue
137+
138+ creator = e .get ("creator" ) or {}
139+ creator_avatar = creator .get ("avatar" )
140+
141+ # Only keep creator avatar if it's GitHub-hosted (can request hi-res)
142+ if _is_github_avatar (creator_avatar ):
143+ creator_avatar = _sharp_github_avatar (creator_avatar , 400 )
135144 else :
136- meta_bits .append (f"by <strong>{ creator_name } </strong>" )
137- if added_at :
138- meta_bits .append (f"added <code>{ added_at } </code>" )
139-
140- if meta_bits :
141- parts .append (" " + " · " .join (meta_bits ) + "<br>" )
142-
143- # Badge row: stars / downloads / updated
144- if repo :
145- parts .append (
146- f' <img src="https://img.shields.io/github/stars/{ repo } ?style=flat&label=stars"> '
147- f'<img src="https://img.shields.io/github/downloads/{ repo } /total?style=flat&label=downloads"> '
148- f'<img src="https://img.shields.io/github/release-date/{ repo } ?label=updated">'
145+ creator_avatar = None # force fallback to owner avatar for sharpness
146+
147+ owner = repo .split ("/" , 1 )[0 ]
148+ owner_avatar = f"https://avatars.githubusercontent.com/{ owner } ?s=400"
149+
150+ # Date -> unix for "added" badge
151+ added_at_raw = e .get ("added_at" )
152+ dt = _parse_date_safe (added_at_raw ) if added_at_raw else datetime .min
153+ added_unix = int (dt .timestamp ()) if dt != datetime .min else None
154+
155+ items .append (
156+ {
157+ "repo" : repo ,
158+ "name" : e .get ("name" ),
159+ "desc" : e .get ("description" , "" ),
160+ "creatorAvatar" : creator_avatar ,
161+ "ownerAvatar" : owner_avatar ,
162+ "addedUnix" : added_unix ,
163+ }
149164 )
165+ return items
150166
151- parts .append (" </td>" )
152- return "\n " .join (parts )
167+
168+ def _render_recent_cards (items : List [Dict [str , Any ]]) -> str :
169+ """
170+ Render a list of items using the exact same layout as the Top 6
171+ cards in README (avatar, title + `plugin`, description, badges),
172+ but using `addedUnix` for a green 'added' date badge.
173+ """
174+ if not items :
175+ return ""
176+
177+ TWO_COL_WIDTH = "50%"
178+ cells : List [str ] = []
179+
180+ for t in items :
181+ # Prefer creator avatar only if it's GitHub-hosted (sharp). Otherwise use owner avatar (400px).
182+ img = t .get ("creatorAvatar" ) or t .get ("ownerAvatar" )
183+ repo = t ["repo" ]
184+ name = md_escape (t .get ("name" ) or repo .split ("/" )[1 ])
185+ desc = md_escape (t .get ("desc" ) or "" )
186+
187+ added_unix = t .get ("addedUnix" )
188+
189+ cell = f"""
190+ <td align="left" valign="top" width="{ TWO_COL_WIDTH } ">
191+ <a href="https://github.com/{ repo } "><img src="{ img } " alt="{ name } " width="100" height="100" style="border-radius:12px;"></a>
192+ <div><strong><a href="https://github.com/{ repo } ">{ name } </a></strong> <code>plugin</code></div>
193+ <div style="margin:4px 0 6px 0;">{ (desc or " " )} </div>
194+ <div>
195+ <img alt="stars" src="https://img.shields.io/github/stars/{ repo } ?style=flat">
196+ <img alt="downloads" src="https://img.shields.io/github/downloads/{ repo } /total?style=flat">"""
197+ if added_unix is not None :
198+ cell += f"""
199+ <img alt="added" src="https://img.shields.io/date/{ added_unix } ?label=added&style=flat">"""
200+ cell += """
201+ </div>
202+ </td>""" .rstrip ()
203+
204+ cells .append (cell )
205+
206+ rows = ["<tr>" + "" .join (cells [i : i + 2 ]) + "</tr>" for i in range (0 , len (cells ), 2 )]
207+ return "\n " .join (["<table>" , * rows , "</table>" ])
153208
154209
155210def generate_recent_plugins_md (entries : List [Dict [str , Any ]]) -> str :
@@ -160,31 +215,19 @@ def generate_recent_plugins_md(entries: List[Dict[str, Any]]) -> str:
160215 <!--- Recently Added Plugins Start -->
161216 <!--- Recently Added Plugins End -->
162217
163- Renders as a 2-column card grid similar to the "Top 6 Plugins" section
164- in the README.
218+ Renders as a 2-column card grid using the Top 6 card layout.
165219 """
166220 if not entries :
167221 return "No recently added plugins found.\n "
168222
223+ items = _recent_items_for_cards (entries )
224+ cards_html = _render_recent_cards (items )
225+
169226 lines : List [str ] = []
170227 lines .append ("> These are the six most recently added plugins (based on `added_at`)." )
171228 lines .append ("" )
172-
173- lines .append ("<table>" )
174- for i in range (0 , len (entries ), 2 ):
175- left = _generate_recent_card (entries [i ])
176- if i + 1 < len (entries ):
177- right = _generate_recent_card (entries [i + 1 ])
178- else :
179- right = " <td></td>"
180-
181- lines .append (" <tr>" )
182- lines .append (left )
183- lines .append (right )
184- lines .append (" </tr>" )
185- lines .append ("</table>" )
229+ lines .append (cards_html )
186230 lines .append ("" )
187-
188231 return "\n " .join (lines )
189232
190233
0 commit comments