5555from ford .console import warn
5656from ford .reader import FortranReader
5757import ford .utils
58- from ford .utils import paren_split , strip_paren
58+ from ford .utils import DOXY_META_RE , paren_split , strip_paren
5959from ford .intrinsics import INTRINSICS
6060from ford ._markdown import MetaMarkdown
6161from ford .settings import ProjectSettings , EntitySettings
103103 "common" : "common" ,
104104}
105105
106+ DOXYGEN_TRANSLATION = {"brief" : "summary" }
107+ DOXYGEN_PARAM_RE = re .compile (
108+ r"""\s*@param\s* # Required command
109+ (?:\[[inout ,]+\]\s*)? # Optional direction (not properly parsed here!)
110+ (?P<name>[\S]*) # Required parameter name
111+ (?P<comment>\s+.*) # Description of parameter
112+ """ ,
113+ re .VERBOSE ,
114+ )
115+ DOXYGEN_SEE_RE = re .compile (r"\s*@see\s*(?P<name>\S+)\s*(?P<comment>[\S\s]*)?" )
116+
106117
107118def _find_in_list (collection : Iterable , name : str ) -> Optional [FortranBase ]:
108119 for item in collection :
@@ -126,6 +137,53 @@ def read_docstring(source: FortranReader, docmark: str) -> List[str]:
126137 return docstring
127138
128139
140+ def translate_links (arr : list [str ]) -> list [str ]:
141+ """Translates Doxygen ``@see`` links to Ford's ``[[]]``"""
142+
143+ for i , doc in enumerate (arr ):
144+ if match := DOXYGEN_SEE_RE .match (doc ):
145+ name = match ["name" ]
146+ comment = f" { match ['comment' ]} " or ""
147+ arr [i ] = f"[[{ name } ]]{ comment } "
148+ return arr
149+
150+
151+ def create_doxy_dict (line : str ) -> dict :
152+ """Create a dictionary of parameters with a name and comment"""
153+
154+ return {m ["name" ]: m ["comment" ] for m in DOXYGEN_PARAM_RE .finditer (line )}
155+
156+
157+ def remove_doxy (source : list ) -> List [str ]:
158+ """Remove doxygen comments with an @ identifier from main comment block."""
159+ return [line for line in source if not DOXYGEN_PARAM_RE .match (line )]
160+
161+
162+ def translate_doxy_meta (doc_list : list [str ]) -> list [str ]:
163+ """Convert doxygen metadata into ford's format"""
164+
165+ # Doxygen commands can appear anywhere, we must move
166+ # to the start of the docstring, and we can't do that
167+ # while iterating on the list
168+ to_be_moved : list [int ] = []
169+
170+ for line , comment in enumerate (doc_list ):
171+ if match := DOXY_META_RE .match (comment ):
172+ meta_type = match ["key" ]
173+ meta_content = match ["value" ].strip ()
174+ meta_type = DOXYGEN_TRANSLATION .get (meta_type , meta_type )
175+ if meta_type != "param" :
176+ doc_list [line ] = f"{ meta_type } : { meta_content } "
177+ to_be_moved .append (line )
178+
179+ for line in to_be_moved :
180+ # This is fine because reorder earlier indices
181+ # doesn't affect later ones
182+ doc_list .insert (0 , doc_list .pop (line ))
183+
184+ return doc_list
185+
186+
129187class Associations :
130188 """
131189 A class for storing associations. Associations are added and removed in batches, akin
@@ -226,6 +284,11 @@ def __init__(
226284
227285 self .base_url = pathlib .Path (self .settings .project_url )
228286 self .doc_list = read_docstring (source , self .settings .docmark )
287+ if self .settings .doxygen :
288+ self .doc_list = translate_links (self .doc_list )
289+
290+ # For line that has been logged in the doc_list we need to check
291+
229292 self .hierarchy = self ._make_hierarchy ()
230293 self .read_metadata ()
231294
@@ -397,6 +460,11 @@ def read_metadata(self):
397460 self .meta = EntitySettings .from_project_settings (self .settings )
398461
399462 if len (self .doc_list ) > 0 :
463+ # we must translate the doxygen metadata into the ford format
464+ # @(meta) (content) ---> (meta): (content)
465+ if self .settings .doxygen :
466+ self .doc_list = translate_doxy_meta (self .doc_list )
467+
400468 if len (self .doc_list ) == 1 and ":" in self .doc_list [0 ]:
401469 words = self .doc_list [0 ].split (":" )[0 ].strip ()
402470 field_names = [field .name for field in fields (EntitySettings )]
@@ -733,7 +801,12 @@ class FortranContainer(FortranBase):
733801 )
734802
735803 def __init__ (
736- self , source , first_line , parent = None , inherited_permission = "public" , strings = []
804+ self ,
805+ source ,
806+ first_line ,
807+ parent = None ,
808+ inherited_permission = "public" ,
809+ strings = [],
737810 ):
738811 self .num_lines = 0
739812 if not isinstance (self , FortranSourceFile ):
@@ -752,6 +825,7 @@ def __init__(
752825 self .VARIABLE_RE = re .compile (
753826 self .VARIABLE_STRING .format (typestr ), re .IGNORECASE
754827 )
828+ self .doxy_dict : Dict [str , str ] = {}
755829
756830 # This is a little bit confusing, because `permission` here is sort of
757831 # overloaded for "permission for this entity", and "permission for child
@@ -767,14 +841,19 @@ def __init__(
767841
768842 blocklevel = 0
769843 associations = Associations ()
770-
771844 for line in source :
772845 if line [0 :2 ] == "!" + self .settings .docmark :
773846 self .doc_list .append (line [2 :])
774847 continue
848+
849+ if self .settings .doxygen :
850+ # Parse doxygen commands and remove them from the docstring
851+ for comment in self .doc_list :
852+ self .doxy_dict .update (create_doxy_dict (comment ))
853+ self .doc_list = remove_doxy (self .doc_list )
854+
775855 if line .strip () != "" :
776856 self .num_lines += 1
777-
778857 # Temporarily replace all strings to make the parsing simpler
779858 self .strings = []
780859 search_from = 0
@@ -2991,6 +3070,10 @@ def line_to_variables(source, line, inherit_permission, parent):
29913070 )
29923071 search_from += QUOTES_RE .search (initial [search_from :]).end (0 )
29933072
3073+ # If the parent has a doxygen `@param` command, add it to any Ford docstring
3074+ if doxy_doc := parent .doxy_dict .pop (name , None ):
3075+ doc .append (doxy_doc )
3076+
29943077 varlist .append (
29953078 FortranVariable (
29963079 name ,
0 commit comments