2626from .utils import Argument , extract_qualifiers , parse_qualified_path
2727
2828
29+ def decode_doxygen_template_encoding (encoded : str ) -> str :
30+ """Decode Doxygen's encoding for template specializations in refids.
31+
32+ Doxygen encodes special characters in refids using underscore-prefixed codes:
33+ - '_3' = '<' (template open)
34+ - '_4' = '>' (template close)
35+ - '_01' = ' ' (space)
36+ - '_07' = '(' (open paren)
37+ - '_08' = ')' (close paren)
38+ - '_8_8_8' = '...' (variadic ellipsis)
39+ - '_00' = ',' (comma)
40+ - '_02' = '*' (pointer)
41+ - '_05' = '=' (equals)
42+ - '_06' = '&' (reference)
43+
44+ e.g. 'SyncCallback_3_01R_07Args_8_8_8_08_4' -> 'SyncCallback< R(Args...)>'
45+ """
46+ result = encoded
47+
48+ # Process longer patterns first to avoid partial matches
49+ result = result .replace ("_8_8_8" , "..." ) # Variadic ellipsis
50+
51+ # Process two-char patterns (_0X codes)
52+ result = result .replace ("_00" , ", " ) # Comma (with space for readability)
53+ result = result .replace ("_01" , " " ) # Space
54+ result = result .replace ("_02" , "*" ) # Pointer
55+ result = result .replace ("_05" , "=" ) # Equals
56+ result = result .replace ("_06" , "&" ) # Reference
57+ result = result .replace ("_07" , "(" ) # Open paren
58+ result = result .replace ("_08" , ")" ) # Close paren
59+
60+ # Process single-char patterns last
61+ result = result .replace ("_3" , "<" ) # Template open
62+ result = result .replace ("_4" , ">" ) # Template close
63+
64+ return result
65+
66+
67+ def _strip_template_args (name : str ) -> str :
68+ """Strip template arguments from a type name.
69+
70+ e.g. 'SyncCallback< R(Args...)>' -> 'SyncCallback'
71+ """
72+ angle_idx = name .find ("<" )
73+ return name [:angle_idx ].rstrip () if angle_idx != - 1 else name
74+
75+
76+ def _qualify_text_with_refid (text : str , refid : str ) -> str :
77+ """Qualify a text symbol using the namespace extracted from its doxygen refid.
78+
79+ For ref elements, doxygen provides a refid that encodes the fully qualified
80+ path to the referenced symbol. This function extracts the namespace from
81+ that refid and prepends it to the text, avoiding redundant qualification.
82+
83+ Args:
84+ text: The symbol text (e.g., "SyncCallback")
85+ refid: The doxygen refid (e.g., "classfacebook_1_1react_1_1SyncCallback...")
86+
87+ Returns:
88+ The qualified text (e.g., "facebook::react::SyncCallback")
89+ """
90+ ns = extract_namespace_from_refid (refid )
91+
92+ # Skip re-qualification if text is already globally qualified
93+ # (starts with "::") - it's already an absolute path
94+ if not ns or text .startswith (ns ) or text .startswith ("::" ):
95+ return text
96+
97+ # The text may already start with a trailing portion of the namespace.
98+ # For example ns="facebook::react::HighResDuration" and
99+ # text="HighResDuration::zero". We need to find the longest suffix of ns
100+ # that is a prefix of text (on a "::" boundary) and only prepend the
101+ # missing part.
102+ ns_parts = ns .split ("::" )
103+ prepend = ns
104+
105+ for i in range (1 , len (ns_parts )):
106+ suffix = "::" .join (ns_parts [i :])
107+ # Also compare without template args - for template specializations
108+ # like "SyncCallback< R(Args...)>", text "SyncCallback" should match
109+ base_suffix = _strip_template_args (ns_parts [i ])
110+ if (
111+ text .startswith (suffix + "::" )
112+ or text == suffix
113+ or text .startswith (base_suffix + "::" )
114+ or text == base_suffix
115+ ):
116+ prepend = "::" .join (ns_parts [:i ])
117+ break
118+
119+ return prepend + "::" + text
120+
121+
29122def extract_namespace_from_refid (refid : str ) -> str :
30123 """Extract the namespace prefix from a doxygen refid.
31124 e.g. 'namespacefacebook_1_1yoga_1a...' -> 'facebook::yoga'
32125 'structfacebook_1_1react_1_1detail_1_1is__dynamic' -> 'facebook::react::detail::is_dynamic'
126+ 'classfacebook_1_1react_1_1SyncCallback_3_01R_07Args_8_8_8_08_4' -> 'facebook::react::SyncCallback< R(Args...)>'
33127
34128 Doxygen encoding:
35129 - '::' is encoded as '_1_1'
36130 - '_' in identifiers is encoded as '__' (double underscore)
131+ - Template specializations are encoded with hex-like codes (see decode_doxygen_template_encoding)
37132 """
38133 for prefix in ("namespace" , "struct" , "class" , "union" ):
39134 if refid .startswith (prefix ):
@@ -46,6 +141,8 @@ def extract_namespace_from_refid(refid: str) -> str:
46141 # Then replace double underscore with single underscore
47142 # (Doxygen encodes '_' in identifiers as '__')
48143 result = result .replace ("__" , "_" )
144+ # Decode template specialization encodings
145+ result = decode_doxygen_template_encoding (result )
49146 return result
50147 return ""
51148
@@ -86,23 +183,7 @@ def resolve_linked_text_name(
86183 # incorrectly treat symbols in strings as references
87184 refid = getattr (part .value , "refid" , None )
88185 if refid and not in_string :
89- ns = extract_namespace_from_refid (refid )
90- # Skip re-qualification if text is already globally qualified
91- # (starts with "::") - it's already an absolute path
92- if ns and not text .startswith (ns ) and not text .startswith ("::" ):
93- # The text may already start with a trailing portion of
94- # the namespace. For example ns="facebook::react::HighResDuration"
95- # and text="HighResDuration::zero". We need to find the
96- # longest suffix of ns that is a prefix of text (on a "::"
97- # boundary) and only prepend the missing part.
98- ns_parts = ns .split ("::" )
99- prepend = ns
100- for i in range (1 , len (ns_parts )):
101- suffix = "::" .join (ns_parts [i :])
102- if text .startswith (suffix + "::" ) or text == suffix :
103- prepend = "::" .join (ns_parts [:i ])
104- break
105- text = prepend + "::" + text
186+ text = _qualify_text_with_refid (text , refid )
106187
107188 name += text
108189 elif type_def .ref :
0 commit comments