@@ -71,13 +71,43 @@ public static void Write(ApiModel model, string outputDir)
7171
7272 // ── path / href contract ───────────────────────────────────────────────
7373
74+ // Characters that are illegal in a Windows filename. Generic type names like
75+ // GitBase<T> carry '<'/'>', which git refuses to check out on Windows runners
76+ // ("error: invalid path … exit code 128"), so every type name has to be folded
77+ // into a portable path segment before it becomes a file or an href.
78+ private static readonly char [ ] InvalidNameChars = { '<' , '>' , ':' , '"' , '/' , '\\ ' , '|' , '?' , '*' } ;
79+ private static readonly System . Text . RegularExpressions . Regex MultiDashRx =
80+ new ( "-+" , System . Text . RegularExpressions . RegexOptions . Compiled ) ;
81+
82+ /// <summary>
83+ /// Fold a type name into a filesystem-safe path segment (no extension). Nested-type dots
84+ /// are preserved; each dot-separated part has Windows-illegal characters replaced with '-',
85+ /// runs collapsed, and dashes trimmed. e.g. <c>GitBase<T></c> → <c>GitBase-T</c>,
86+ /// <c>GitBase<T>.GitBaseAsset</c> → <c>GitBase-T.GitBaseAsset</c>.
87+ /// </summary>
88+ public static string FileSegment ( string typeName )
89+ {
90+ static string Clean ( string part )
91+ {
92+ var sb = new StringBuilder ( part . Length ) ;
93+ foreach ( var c in part )
94+ sb . Append ( Array . IndexOf ( InvalidNameChars , c ) >= 0 ? '-' : c ) ;
95+ var cleaned = MultiDashRx . Replace ( sb . ToString ( ) , "-" ) . Trim ( '-' ) ;
96+ return cleaned . Length == 0 ? "_" : cleaned ;
97+ }
98+ return string . Join ( "." , typeName . Split ( '.' ) . Select ( Clean ) ) ;
99+ }
100+
74101 /// <summary>
75102 /// Page path relative to the language root for a type. Empty <paramref name="namespaceName"/>
76103 /// (flat languages) → <c>TypeName.md</c>; otherwise <c>Namespace/TypeName.md</c>.
77- /// Adapters use this to populate <see cref="ApiTypeRef.Href"/>.
104+ /// Adapters use this to populate <see cref="ApiTypeRef.Href"/>. The type name is folded
105+ /// through <see cref="FileSegment"/> so the href matches the on-disk (portable) filename.
78106 /// </summary>
79107 public static string PageHref ( string namespaceName , string typeName )
80- => string . IsNullOrEmpty ( namespaceName ) ? $ "{ typeName } .md" : $ "{ namespaceName } /{ typeName } .md";
108+ => string . IsNullOrEmpty ( namespaceName )
109+ ? $ "{ FileSegment ( typeName ) } .md"
110+ : $ "{ namespaceName } /{ FileSegment ( typeName ) } .md";
81111
82112 /// <summary>Page path + member anchor, relative to the language root.</summary>
83113 public static string MemberHref ( string namespaceName , string typeName , string anchor )
@@ -268,7 +298,7 @@ private static void AppendTypeTables(StringBuilder sb, IReadOnlyList<ApiType> ty
268298 sb . AppendLine ( ) ;
269299 var rows = group . Select ( t => ( IReadOnlyList < string > ) new [ ]
270300 {
271- $ "[`{ RenderHelpers . InlineCodeEscape ( t . Name ) } `](./{ t . Name } .md)" +
301+ $ "[`{ RenderHelpers . InlineCodeEscape ( t . Name ) } `](./{ FileSegment ( t . Name ) } .md)" +
272302 ( t . IsObsolete ? " _(deprecated)_" : "" ) ,
273303 RenderHelpers . CellEscape ( FirstLine ( t . Summary ) ) ,
274304 } ) ;
@@ -355,7 +385,7 @@ private static void WriteTypePage(ApiModel model, ApiNamespace ns, ApiType type,
355385 sb . AppendLine ( "___" ) ;
356386 sb . AppendLine ( $ "*Generated from `{ model . PackageName } `{ VersionSuffix ( model ) } *") ;
357387
358- File . WriteAllText ( Path . Combine ( dir , type . Name + ".md" ) , sb . ToString ( ) ) ;
388+ File . WriteAllText ( Path . Combine ( dir , FileSegment ( type . Name ) + ".md" ) , sb . ToString ( ) ) ;
359389 }
360390
361391 private static void AppendInheritance ( StringBuilder sb , ApiType type , string currentPageDir )
0 commit comments