@@ -130,15 +130,19 @@ def get_term_url(label, short_form):
130130 url = label .replace ('\\ ' , '' ).replace (' ' , '-' ).lower () + "-" + short_form .lower ()
131131 return re .sub ("[^0-9a-zA-Z-_]+" , "" , url )
132132
133+ def get_report_url (identifier ):
134+ """Create the VFB report URL path for a term identifier."""
135+ return f'/reports/{ identifier } '
136+
133137def is_known_id (identifier ):
134138 """Check if an identifier matches a known VFB prefix."""
135139 return any (identifier .startswith (p ) for p in KNOWN_PREFIXES )
136140
137141def convert_internal_links (text ):
138- """Convert API markdown links [label](ID) to site inter-page links.
142+ """Convert API markdown links [label](ID) to VFB report links.
139143
140144 Handles:
141- - Simple links: [medulla](FBbt_00003748) → [medulla](/term/slug/ )
145+ - Simple links: [medulla](FBbt_00003748) → [medulla](/reports/FBbt_00003748 )
142146 - IDs that aren't known prefixes are left as-is
143147 """
144148 if not text :
@@ -148,8 +152,7 @@ def replace_link(match):
148152 label = match .group (1 )
149153 identifier = match .group (2 )
150154 if is_known_id (identifier ):
151- slug = get_term_url (label , identifier )
152- return f'[{ label } ](/term/{ slug } /)'
155+ return f'[{ label } ]({ get_report_url (identifier )} )'
153156 return match .group (0 )
154157
155158 # Use a pattern that handles nested brackets in labels (e.g. gene names with [allele])
@@ -160,7 +163,7 @@ def format_relationships_section(relationships_text):
160163 """Format Meta.Relationships into markdown bullets.
161164
162165 Input format: [rel_name](rel_id): [target1](id1), [target2](id2); [rel2](id): [target](id)
163- Output: - **rel_name**: [target1](/term /...), [target2](/term /...)
166+ Output: - **rel_name**: [target1](/reports /...), [target2](/reports /...)
164167 """
165168 if not relationships_text :
166169 return ""
@@ -192,8 +195,8 @@ def format_types_section(types_text):
192195 """Format Meta.Types into markdown bullets.
193196
194197 Input format: [type1](id1); [type2](id2)
195- Output: - [type1](/term/slug1/ )
196- - [type2](/term/slug2/ )
198+ Output: - [type1](/reports/id1 )
199+ - [type2](/reports/id2 )
197200 """
198201 if not types_text :
199202 return ""
@@ -291,6 +294,9 @@ def format_query_preview(query, term_id):
291294 # Extract name — may be markdown link like [name](id)
292295 name_raw = row .get ("label" , row .get ("name" , "" ))
293296 name_converted = convert_internal_links (name_raw )
297+ row_id = row .get ("id" , "" )
298+ if row_id and is_known_id (row_id ) and name_converted == name_raw and name_raw :
299+ name_converted = f'[{ name_raw } ]({ get_report_url (row_id )} )'
294300
295301 # Extract tags
296302 tags_raw = row .get ("tags" , "" )
@@ -304,7 +310,6 @@ def format_query_preview(query, term_id):
304310 img_match = re .search (r'!\[[^\]]*\]\(([^\s)]+)' , thumb_raw )
305311 if img_match :
306312 thumb_url = img_match .group (1 )
307- row_id = row .get ("id" , "" )
308313 thumb_html = f'<a href="{ VFB_BROWSER_BASE } ?id={ row_id } "><img src="{ thumb_url } " width="80" style="background:#000; border-radius:2px;"/></a>'
309314
310315 lines .append (f'| { thumb_html } | { name_converted } | { tags_display } |' )
@@ -389,6 +394,45 @@ def format_publications(publications):
389394 lines .append ("" .join (parts ))
390395 return "\n " .join (lines )
391396
397+ def build_hero_card (name , term_id , tags_badges , description_html , comment_html , thumbnails ):
398+ """Build the hero card without blank lines that break Markdown HTML blocks."""
399+ lines = [
400+ '<div class="card mb-4 border-primary">' ,
401+ '<div class="card-body">' ,
402+ '<div class="row">' ,
403+ '<div class="col-md-4 text-center">' ,
404+ ]
405+
406+ for thumb in thumbnails :
407+ lines .append (
408+ f' <a href="{ VFB_BROWSER_BASE } ?id={ term_id } ">'
409+ f'<img src="{ thumb ["url" ]} " alt="{ name } " class="img-fluid rounded" '
410+ f'style="max-width:200px; background:#000; margin:4px;"/></a>'
411+ )
412+
413+ lines .extend ([
414+ '</div>' ,
415+ '<div class="col-md-8">' ,
416+ f' <h4>{ name } </h4>' ,
417+ f' <p class="text-muted"><strong>ID:</strong> { term_id } </p>' ,
418+ f' <div class="mb-2">{ tags_badges } </div>' ,
419+ ])
420+
421+ if description_html :
422+ lines .append (f' { description_html } ' )
423+ if comment_html :
424+ lines .append (f' { comment_html } ' )
425+
426+ lines .extend ([
427+ f' <a href="{ VFB_BROWSER_BASE } ?id={ term_id } " class="btn btn-primary btn-lg mt-2">Open in VFB 3D Browser →</a>' ,
428+ '</div>' ,
429+ '</div>' ,
430+ '</div>' ,
431+ '</div>' ,
432+ ])
433+
434+ return "\n " .join (lines )
435+
392436# ─── Page Generation ─────────────────────────────────────────────────────────
393437
394438def generate_page (term_data ):
@@ -448,17 +492,6 @@ def generate_page(term_data):
448492''' )
449493
450494 # ── Hero section ──
451- thumb_html = ""
452- if thumbnails :
453- thumb_imgs = []
454- for t in thumbnails :
455- thumb_imgs .append (
456- f'<a href="{ VFB_BROWSER_BASE } ?id={ term_id } ">'
457- f'<img src="{ t ["url" ]} " alt="{ name } " class="img-fluid rounded" '
458- f'style="max-width:200px; background:#000; margin:4px;"/></a>'
459- )
460- thumb_html = "\n " .join (thumb_imgs )
461-
462495 tags_badges = format_tags_badges (tags )
463496
464497 desc_html = ""
@@ -469,24 +502,7 @@ def generate_page(term_data):
469502 if comment :
470503 comment_html = f'<p class="text-muted"><em>{ convert_internal_links (comment )} </em></p>'
471504
472- sections .append (f'''<div class="card mb-4 border-primary">
473- <div class="card-body">
474- <div class="row">
475- <div class="col-md-4 text-center">
476- { thumb_html }
477- </div>
478- <div class="col-md-8">
479- <h4>{ name } </h4>
480- <p class="text-muted"><strong>ID:</strong> { term_id } </p>
481- <div class="mb-2">{ tags_badges } </div>
482- { desc_html }
483- { comment_html }
484- <a href="{ VFB_BROWSER_BASE } ?id={ term_id } " class="btn btn-primary btn-lg mt-2">Open in VFB 3D Browser →</a>
485- </div>
486- </div>
487- </div>
488- </div>
489- ''' )
505+ sections .append (build_hero_card (name , term_id , tags_badges , desc_html , comment_html , thumbnails ))
490506
491507 # ── Classification ──
492508 types_text = meta .get ("Types" , "" )
@@ -647,21 +663,115 @@ def test_term_page(term_id, term_type="class"):
647663
648664 return all_pass
649665
666+ def test_hero_card_regression ():
667+ """Ensure optional empty fields do not turn the hero CTA into a code block."""
668+ print ("=" * 60 )
669+ print ("Test 0: Hero card CTA markdown regression" )
670+ print ("=" * 60 )
671+
672+ term_data = {
673+ "Name" : "adult intercalary segment" ,
674+ "Id" : "FBbt_00003013" ,
675+ "Meta" : {
676+ "Description" : "Any intercalary segment of the adult." ,
677+ "Comment" : "" ,
678+ "Types" : "[adult procephalic segment](FBbt_00003010); [intercalary segment](FBbt_00000010)" ,
679+ },
680+ "Tags" : ["Adult" , "Anatomy" ],
681+ "Synonyms" : [],
682+ "Queries" : [],
683+ "Licenses" : {},
684+ "Publications" : [],
685+ "Technique" : [],
686+ "Images" : {},
687+ "Examples" : {},
688+ }
689+
690+ page_content = generate_page (term_data )
691+ hero_cta = f'<a href="{ VFB_BROWSER_BASE } ?id=FBbt_00003013" class="btn btn-primary btn-lg mt-2">Open in VFB 3D Browser →</a>'
692+
693+ checks = {
694+ "Hero CTA present" : hero_cta in page_content ,
695+ "No blank line before hero CTA" : "\n \n <a href=" not in page_content ,
696+ "Description directly precedes hero CTA" : (
697+ '<p>Any intercalary segment of the adult.</p>\n '
698+ f' { hero_cta } '
699+ ) in page_content ,
700+ }
701+
702+ all_pass = True
703+ for check_name , result in checks .items ():
704+ status = "PASS" if result else "FAIL"
705+ if not result :
706+ all_pass = False
707+ print (f" [{ status } ] { check_name } " )
708+
709+ return all_pass
710+
711+ def test_report_link_regression ():
712+ """Ensure generated term links point at VFB report URLs."""
713+ print ("=" * 60 )
714+ print ("Test 1: Report link regression" )
715+ print ("=" * 60 )
716+
717+ converted = convert_internal_links ("[DNb08](FBbt_20011340)" )
718+ query_preview = format_query_preview (
719+ {
720+ "label" : "Neurons with some part in adult intercalary segment" ,
721+ "count" : 2 ,
722+ "preview_results" : {
723+ "rows" : [
724+ {
725+ "label" : "[DNb08](FBbt_20011340)" ,
726+ "id" : "FBbt_20011340" ,
727+ "tags" : "Adult|Cholinergic|Nervous_system|primary_neuron" ,
728+ "thumbnail" : '[](FBbt_20011340)' ,
729+ },
730+ {
731+ "label" : "DNp45" ,
732+ "id" : "FBbt_20011346" ,
733+ "tags" : "Adult|Cholinergic|Nervous_system|primary_neuron" ,
734+ },
735+ ]
736+ },
737+ },
738+ "FBbt_00003013" ,
739+ )
740+
741+ checks = {
742+ "Markdown links use report path" : converted == "[DNb08](/reports/FBbt_20011340)" ,
743+ "Query preview preserves report link" : "[DNb08](/reports/FBbt_20011340)" in query_preview ,
744+ "Plain row labels get report link" : "[DNp45](/reports/FBbt_20011346)" in query_preview ,
745+ }
746+
747+ all_pass = True
748+ for check_name , result in checks .items ():
749+ status = "PASS" if result else "FAIL"
750+ if not result :
751+ all_pass = False
752+ print (f" [{ status } ] { check_name } " )
753+
754+ return all_pass
755+
650756def test_medulla_page ():
651757 """Test page generation for medulla (class) and fru-M-200266 (individual)."""
758+ result0 = test_hero_card_regression ()
759+ result1 = test_report_link_regression ()
760+
761+ print ()
652762 print ("=" * 60 )
653- print ("Test 1 : Class term — medulla (FBbt_00003748)" )
763+ print ("Test 2 : Class term — medulla (FBbt_00003748)" )
654764 print ("=" * 60 )
655- result1 = test_term_page ("FBbt_00003748" , "class" )
765+ result2 = test_term_page ("FBbt_00003748" , "class" )
656766
657767 print ()
658768 print ("=" * 60 )
659- print ("Test 2 : Individual term — fru-M-200266 (VFB_00000001)" )
769+ print ("Test 3 : Individual term — fru-M-200266 (VFB_00000001)" )
660770 print ("=" * 60 )
661- result2 = test_term_page ("VFB_00000001" , "individual" )
771+ result3 = test_term_page ("VFB_00000001" , "individual" )
662772
663773 print ()
664- if result1 and result2 :
774+ if result0 and result1 and result2 and result3 :
665775 print ("All tests PASSED" )
666776 return True
667777 else :
0 commit comments