Skip to content

Commit d678035

Browse files
committed
Sanitize invalid file names for windows
1 parent a827231 commit d678035

4 files changed

Lines changed: 36 additions & 6 deletions

File tree

docs/reference/cs/Velopack.Sources/GitBase<T>.GitBaseAsset.md renamed to docs/reference/cs/Velopack.Sources/GitBase-T.GitBaseAsset.md

File renamed without changes.
File renamed without changes.

docs/reference/cs/Velopack.Sources/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ sidebar_position: 0
1111

1212
| Type | Summary |
1313
| --- | --- |
14-
| [`GitBase<T>`](./GitBase<T>.md) | Base class to provide some shared implementation between sources which download releases from a Git repository. |
15-
| [`GitBase<T>.GitBaseAsset`](./GitBase<T>.GitBaseAsset.md) | Provides a wrapper around `VelopackAsset` which also contains a Git Release. |
14+
| [`GitBase<T>`](./GitBase-T.md) | Base class to provide some shared implementation between sources which download releases from a Git repository. |
15+
| [`GitBase<T>.GitBaseAsset`](./GitBase-T.GitBaseAsset.md) | Provides a wrapper around `VelopackAsset` which also contains a Git Release. |
1616
| [`GiteaRelease`](./GiteaRelease.md) | Describes a Gitea release, including attached assets. |
1717
| [`GiteaReleaseAsset`](./GiteaReleaseAsset.md) | Describes a asset (file) uploaded to a Gitea release. |
1818
| [`GiteaSource`](./GiteaSource.md) | Retrieves available releases from a Gitea repository. |

generator/Markdown/MarkdownWriter.cs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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&lt;T&gt;</c> → <c>GitBase-T</c>,
86+
/// <c>GitBase&lt;T&gt;.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

Comments
 (0)