5858DESCRIPTION_MISSING = '<span style="color:grey">*no description yet*</span>'
5959TOO_MANY_TESTS_TO_SHOW = 200
6060TESTS_TO_SHOW_IF_TOO_MANY = 20
61+ USED_ANCHORS = set ()
6162
6263
6364# Data class to store which sections belong to which chapter (and thus to which file)
@@ -74,6 +75,48 @@ class ReferenceChapter:
7475 has_content : bool = False # Whether the chapter has any content (filled later)
7576
7677
78+ def sanitize_anchor (text : str ):
79+ """
80+ Sanitize a string so it is safe and consistent to use as an explicit MyST anchor.
81+
82+ - Strip trailing HTML (e.g. ", <span ...>").
83+ - Remove '<' and '>'.
84+ - Remove whitespace.
85+ - Replace '/' with '_'.
86+ """
87+ # cut off HTML decoration like ", <span style=...>"
88+ text = re .sub (r"\s*, <span.*" , "" , text ).strip ()
89+ text = text .replace ("<" , "" ).replace (">" , "" )
90+ text = text .replace (" " , "" )
91+ text = text .replace ("/" , "_" )
92+ return text
93+
94+
95+ def make_anchor_base (option_name : str , path_prefix : str | None ):
96+ """
97+ Build the base anchor from an option name and an optional hierarchical prefix.
98+ """
99+ base = sanitize_anchor (option_name )
100+ if path_prefix :
101+ base = f"{ path_prefix } _{ base } "
102+ return base
103+
104+
105+ def make_unique_anchor (base : str ):
106+ if base not in USED_ANCHORS :
107+ USED_ANCHORS .add (base )
108+ return base
109+
110+ n = 2
111+ candidate = f"{ base } -{ n } "
112+ while candidate in USED_ANCHORS :
113+ n += 1
114+ candidate = f"{ base } -{ n } "
115+
116+ USED_ANCHORS .add (candidate )
117+ return candidate
118+
119+
77120def make_collapsible (content , summary = None , open_content = None ):
78121 documentation = []
79122 if open_content is None :
@@ -247,7 +290,9 @@ def primitive_to_md(primitive: Primitive, make_description=make_spec_description
247290 )
248291
249292
250- def vector_or_map_to_md (entry : Vector | Map , make_description = make_spec_description ):
293+ def vector_or_map_to_md (
294+ entry : Vector | Map , make_description = make_spec_description , path_prefix : str = ""
295+ ):
251296 type_description = type_or_none (value_type_flatter (entry ), entry )
252297
253298 header_properties = {"size" : flatten_size_vector (entry ), "required" : entry .required }
@@ -260,7 +305,9 @@ def vector_or_map_to_md(entry: Vector | Map, make_description=make_spec_descript
260305 collapsible_properties = None
261306 if not isinstance (entry .value_type , Primitive ):
262307 collapsible_properties = {
263- "Value type" : all_of_to_md (All_Of ([entry .value_type ]))
308+ "Value type" : all_of_to_md (
309+ All_Of ([entry .value_type ]), path_prefix = path_prefix
310+ )
264311 }
265312
266313 return make_description (
@@ -272,7 +319,9 @@ def vector_or_map_to_md(entry: Vector | Map, make_description=make_spec_descript
272319 )
273320
274321
275- def tuple_to_md (tuple_entry : Tuple , make_description = make_spec_description ):
322+ def tuple_to_md (
323+ tuple_entry : Tuple , make_description = make_spec_description , path_prefix : str = ""
324+ ):
276325 type_description = type_or_none (value_type_flatter (tuple_entry ), tuple_entry )
277326
278327 header_properties = {"required" : tuple_entry .required }
@@ -282,7 +331,9 @@ def tuple_to_md(tuple_entry: Tuple, make_description=make_spec_description):
282331 header_properties ["default" ] = none_to_null (tuple_entry .default )
283332
284333 collapsible_properties = {
285- "Entries" : all_of_to_md (All_Of (tuple_entry .value_types ), indent = 1 )
334+ "Entries" : all_of_to_md (
335+ All_Of (tuple_entry .value_types ), indent = 1 , path_prefix = path_prefix
336+ )
286337 }
287338
288339 return make_description (
@@ -320,12 +371,16 @@ def enum_to_md(enum: Enum, make_description=make_spec_description):
320371 )
321372
322373
323- def group_to_md (group : Group , make_description = make_spec_description ):
374+ def group_to_md (
375+ group : Group , make_description = make_spec_description , path_prefix : str = ""
376+ ):
324377 type_description = type_or_none (group .spec_type , group )
325378
326379 collapsible_properties = {}
327380 if len (group .spec ) > 0 :
328- collapsible_properties = {"Contains" : all_of_to_md (group .spec , indent = 1 )}
381+ collapsible_properties = {
382+ "Contains" : all_of_to_md (group .spec , indent = 1 , path_prefix = path_prefix )
383+ }
329384
330385 header_properties = validator_to_header_argument (group .validator )
331386 return make_description (
@@ -337,7 +392,9 @@ def group_to_md(group: Group, make_description=make_spec_description):
337392 )
338393
339394
340- def list_to_md (list_entry : List , make_description = make_spec_description ):
395+ def list_to_md (
396+ list_entry : List , make_description = make_spec_description , path_prefix : str = ""
397+ ):
341398 type_description = type_or_none (list_entry .spec_type , list_entry )
342399
343400 header_properties = {}
@@ -351,18 +408,22 @@ def list_to_md(list_entry: List, make_description=make_spec_description):
351408 header_properties ,
352409 list_entry .description ,
353410 collapsible_properties = {
354- "Each element contains" : all_of_to_md (list_entry .spec , indent = 1 )
411+ "Each element contains" : all_of_to_md (
412+ list_entry .spec , indent = 1 , path_prefix = path_prefix
413+ )
355414 },
356415 )
357416
358417
359- def selection_to_md (selection : Selection , make_description = make_spec_description ):
418+ def selection_to_md (
419+ selection : Selection , make_description = make_spec_description , path_prefix : str = ""
420+ ):
360421 type_description = type_or_none (selection .spec_type , selection )
361422
362423 choices = ""
363424 for choice , choice_spec in selection .choices .items ():
364425 choices += "\n \n " + " " + f"- *{ choice } *:\n \n "
365- choices += all_of_to_md (choice_spec , indent = 3 )
426+ choices += all_of_to_md (choice_spec , indent = 3 , path_prefix = path_prefix )
366427
367428 return make_description (
368429 selection .name ,
@@ -391,22 +452,31 @@ def sort_one_of_option_names(one_of: One_Of) -> list:
391452 return [", " .join (l ) for l in options ]
392453
393454
394- def one_of_to_md (one_of : One_Of ):
455+ def one_of_to_md (one_of : One_Of , path_prefix : str = "" ):
395456 header = "*One of*"
396457
397458 description = ""
398459 if check_if_set (one_of .description ):
399- description += "\n " + description + "\n "
460+ description += "\n " + one_of . description + "\n "
400461
401462 open_content = len (one_of .specs ) < 11
402463
403464 names = sort_one_of_option_names (one_of )
404465
405466 for i , spec in enumerate (one_of .specs ):
467+ anchor_base = make_anchor_base (names [i ], path_prefix )
468+ opt_anchor = make_unique_anchor (anchor_base )
469+
406470 key = "Option (" + names [i ] + ")"
407- description += "\n " + make_collapsible (
408- textwrap .indent (all_of_to_md (spec , 1 ), " " ), key , open_content
471+
472+ # Handle nested One Of
473+ nested_prefix = anchor_base
474+ content = textwrap .indent (
475+ all_of_to_md (spec , indent = 1 , path_prefix = nested_prefix ),
476+ " " ,
409477 )
478+ content = f"({ opt_anchor } )=\n \n " + content
479+ description += "\n " + make_collapsible (content , key , open_content )
410480
411481 return header , description
412482
@@ -450,7 +520,7 @@ def validator_to_header_argument(validator):
450520 raise ValueError (f"Unknown validator { validator } " )
451521
452522
453- def all_of_to_md (all_of : All_Of , indent = 0 ):
523+ def all_of_to_md (all_of : All_Of , indent = 0 , path_prefix : str = "" ):
454524 entries = ""
455525 for entry in all_of :
456526 string_entry = None
@@ -459,19 +529,31 @@ def all_of_to_md(all_of: All_Of, indent=0):
459529 case Primitive ():
460530 string_entry , description_entry = primitive_to_md (entry )
461531 case Vector () | Map ():
462- string_entry , description_entry = vector_or_map_to_md (entry )
532+ string_entry , description_entry = vector_or_map_to_md (
533+ entry , path_prefix = path_prefix
534+ )
463535 case Tuple ():
464- string_entry , description_entry = tuple_to_md (entry )
536+ string_entry , description_entry = tuple_to_md (
537+ entry , path_prefix = path_prefix
538+ )
465539 case Enum ():
466540 string_entry , description_entry = enum_to_md (entry )
467541 case Group ():
468- string_entry , description_entry = group_to_md (entry )
542+ string_entry , description_entry = group_to_md (
543+ entry , path_prefix = path_prefix
544+ )
469545 case List ():
470- string_entry , description_entry = list_to_md (entry )
546+ string_entry , description_entry = list_to_md (
547+ entry , path_prefix = path_prefix
548+ )
471549 case Selection ():
472- string_entry , description_entry = selection_to_md (entry )
550+ string_entry , description_entry = selection_to_md (
551+ entry , path_prefix = path_prefix
552+ )
473553 case One_Of ():
474- string_entry , description_entry = one_of_to_md (entry )
554+ string_entry , description_entry = one_of_to_md (
555+ entry , path_prefix = path_prefix
556+ )
475557 case _:
476558 raise ValueError (type (entry ))
477559
@@ -487,13 +569,11 @@ def all_of_to_md(all_of: All_Of, indent=0):
487569
488570
489571def create_section_markdown (section , section_in_tests , section_in_tutorials ):
490- # link anchor
491- replacements = [(" " , "" ), ("/" , "_" ), ("<" , "" ), (">" , "" )]
492- section_link_anchor = "sec" + section .name .lower ()
493- for old , new in replacements :
494- section_link_anchor = section_link_anchor .replace (old , new )
572+ section_link_anchor = "sec" + sanitize_anchor (section .name .lower ())
495573 string_entry = "\n (" + section_link_anchor + ")=\n \n "
496574
575+ section_prefix = sanitize_anchor (section .name .upper ())
576+
497577 create_section = partial (
498578 make_section_description ,
499579 section_in_tests = section_in_tests ,
@@ -504,19 +584,30 @@ def create_section_markdown(section, section_in_tests, section_in_tutorials):
504584 case Primitive ():
505585 string_entry += primitive_to_md (section , create_section )
506586 case Vector () | Map ():
507- string_entry += vector_or_map_to_md (section , create_section )
587+ string_entry += vector_or_map_to_md (
588+ section , create_section , path_prefix = section_prefix
589+ )
508590 case Tuple ():
509- string_entry += tuple_to_md (section , create_section )
591+ string_entry += tuple_to_md (
592+ section , create_section , path_prefix = section_prefix
593+ )
510594 case Enum ():
511595 string_entry += enum_to_md (section , create_section )
512596 case Group ():
513- string_entry += group_to_md (section , create_section )
597+ string_entry += group_to_md (
598+ section , create_section , path_prefix = section_prefix
599+ )
514600 case List ():
515- string_entry += list_to_md (section , create_section )
601+ string_entry += list_to_md (
602+ section , create_section , path_prefix = section_prefix
603+ )
516604 case Selection ():
517- string_entry += selection_to_md (section , create_section )
605+ string_entry += selection_to_md (
606+ section , create_section , path_prefix = section_prefix
607+ )
518608 case One_Of ():
519- string_entry += one_of_to_md (section , create_section )
609+ header , description = one_of_to_md (section , path_prefix = section_prefix )
610+ string_entry += header + "\n " + description + "\n "
520611 case _:
521612 raise ValueError (type (section ))
522613 return string_entry + "\n \n "
0 commit comments