Skip to content

Commit 0a52f75

Browse files
committed
Add validation and title as mandatory
1 parent b0e6ec3 commit 0a52f75

File tree

3 files changed

+43
-31
lines changed

3 files changed

+43
-31
lines changed

src/Elastic.Documentation.Configuration/Builder/TableOfContentsConfiguration.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,22 @@ private IReadOnlyCollection<ITocItem> ReadChildren(YamlStreamReader reader, KeyV
156156
case "crosslink":
157157
hiddenFile = false;
158158
crossLink = reader.ReadString(entry);
159+
// Validate crosslink URI early
160+
if (string.IsNullOrWhiteSpace(crossLink))
161+
{
162+
reader.EmitError("Cross-link entries must specify a non-empty 'crosslink' URI", tocEntry);
163+
crossLink = null; // Reset to prevent further processing
164+
}
165+
else if (!Uri.TryCreate(crossLink, UriKind.Absolute, out var parsedUri))
166+
{
167+
reader.EmitError($"Cross-link URI '{crossLink}' is not a valid absolute URI format", tocEntry);
168+
crossLink = null; // Reset to prevent further processing
169+
}
170+
else if (parsedUri.Scheme is "http" or "https" or "ftp" or "file")
171+
{
172+
reader.EmitError($"Cross-link URI '{crossLink}' cannot use standard web schemes (http, https, ftp, file). Use cross-repository schemes like 'docs-content://', 'kibana://', etc.", tocEntry);
173+
crossLink = null; // Reset to prevent further processing
174+
}
159175
break;
160176
case "folder":
161177
folder = ReadFolder(reader, entry, parentPath);
@@ -174,6 +190,13 @@ private IReadOnlyCollection<ITocItem> ReadChildren(YamlStreamReader reader, KeyV
174190
}
175191
}
176192

193+
// Validate that crosslink entries have titles
194+
if (crossLink is not null && string.IsNullOrWhiteSpace(title))
195+
{
196+
reader.EmitError($"Cross-link entries must have a 'title' specified. Cross-link: {crossLink}", tocEntry);
197+
return null;
198+
}
199+
177200
if (toc is not null)
178201
{
179202
foreach (var f in toc.Files)

src/Elastic.Markdown/IO/Navigation/CrossLinkNavigationItem.cs

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,44 +13,15 @@ public record CrossLinkNavigationItem : ILeafNavigationItem<INavigationModel>
1313
{
1414
// Override Url accessor to use ResolvedUrl if available
1515
string INavigationItem.Url => ResolvedUrl ?? Url;
16-
public CrossLinkNavigationItem(string url, string? title, DocumentationGroup group, bool hidden = false)
16+
public CrossLinkNavigationItem(string url, string title, DocumentationGroup group, bool hidden = false)
1717
{
1818
_url = url;
19-
NavigationTitle = title ?? GetNavigationTitleFromUrl(url);
19+
NavigationTitle = title;
2020
Parent = group;
2121
NavigationRoot = group.NavigationRoot;
2222
Hidden = hidden;
2323
}
2424

25-
private string GetNavigationTitleFromUrl(string url)
26-
{
27-
// Extract a decent title from the URL
28-
try
29-
{
30-
if (Uri.TryCreate(url, UriKind.Absolute, out var uri))
31-
{
32-
// Get the last segment of the path and remove extension
33-
var lastSegment = uri.AbsolutePath.Split('/').Last();
34-
lastSegment = Path.GetFileNameWithoutExtension(lastSegment);
35-
36-
// Convert to title case (simple version)
37-
if (!string.IsNullOrEmpty(lastSegment))
38-
{
39-
var words = lastSegment.Replace('-', ' ').Replace('_', ' ').Split(' ');
40-
var titleCase = string.Join(" ", words.Select(w =>
41-
string.IsNullOrEmpty(w) ? "" : char.ToUpper(w[0]) + w[1..].ToLowerInvariant()));
42-
return titleCase;
43-
}
44-
}
45-
}
46-
catch
47-
{
48-
// Fall back to URL if parsing fails
49-
}
50-
51-
return url;
52-
}
53-
5425
public INodeNavigationItem<INavigationModel, INavigationItem>? Parent { get; set; }
5526
public IRootNavigationItem<INavigationModel, INavigationItem> NavigationRoot { get; }
5627
// Original URL from the cross-link

src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,30 @@ void AddToNavigationItems(INavigationItem item, ref int fileIndex)
123123
{
124124
if (tocItem is CrossLinkReference crossLink)
125125
{
126+
// Validate that cross-link URI is not empty
127+
if (string.IsNullOrWhiteSpace(crossLink.CrossLinkUri))
128+
{
129+
context.EmitError(context.ConfigurationPath,
130+
"Cross-link entries must have a 'crosslink' URI specified.");
131+
continue;
132+
}
133+
134+
// Validate that cross-link URI is a valid URI format
135+
if (!Uri.TryCreate(crossLink.CrossLinkUri, UriKind.Absolute, out var parsedUri))
136+
{
137+
context.EmitError(context.ConfigurationPath,
138+
$"Cross-link URI '{crossLink.CrossLinkUri}' is not a valid absolute URI format.");
139+
continue;
140+
}
141+
142+
// Validate that cross-link has a title
126143
if (string.IsNullOrWhiteSpace(crossLink.Title))
127144
{
128145
context.EmitError(context.ConfigurationPath,
129146
$"Cross-link entries must have a 'title' specified. Cross-link: {crossLink.CrossLinkUri}");
130147
continue;
131148
}
149+
132150
// Create a special navigation item for cross-repository links
133151
var crossLinkItem = new CrossLinkNavigationItem(crossLink.CrossLinkUri, crossLink.Title, this, crossLink.Hidden);
134152
AddToNavigationItems(crossLinkItem, ref fileIndex);

0 commit comments

Comments
 (0)