@@ -68,6 +68,11 @@ public record ConfigurationFile
6868 /// </summary>
6969 public HashSet < HintType > SuppressDiagnostics { get ; } = [ ] ;
7070
71+ /// <summary>
72+ /// White-label branding overrides. When non-null, all Elastic-specific chrome is suppressed.
73+ /// </summary>
74+ public BrandingConfiguration ? Branding { get ; private set ; }
75+
7176 /// This is a documentation set not linked to by assembler.
7277 /// Setting this to true relaxes a few restrictions such as mixing toc references with file and folder reference
7378 public bool DevelopmentDocs { get ; }
@@ -248,13 +253,21 @@ public ConfigurationFile(DocumentationSetFile docSetFile, IDocumentationSetConte
248253 . ToHashSet ( ) ! ;
249254 }
250255
256+ // Process branding with validation
257+ if ( docSetFile . Branding is not null )
258+ Branding = ValidateBranding ( docSetFile . Branding , context ) ;
259+
251260 // Process features
252261 _features = new Dictionary < string , bool > ( StringComparer . OrdinalIgnoreCase ) ;
253262 if ( docSetFile . Features . PrimaryNav . HasValue )
254263 _features [ "primary-nav" ] = docSetFile . Features . PrimaryNav . Value ;
255264 if ( docSetFile . Features . DisableGithubEditLink . HasValue )
256265 _features [ "disable-github-edit-link" ] = docSetFile . Features . DisableGithubEditLink . Value ;
257266
267+ // primary-nav requires the Elastic global navigation which is not available for white-label builds
268+ if ( Branding is not null && docSetFile . Features . PrimaryNav is true )
269+ context . EmitError ( context . ConfigurationPath , "'features.primary-nav' cannot be used together with 'branding': the primary nav requires Elastic global navigation." ) ;
270+
258271 // Add version substitutions
259272 foreach ( var ( id , system ) in versionsConfig . VersioningSystems )
260273 {
@@ -283,6 +296,56 @@ public ConfigurationFile(DocumentationSetFile docSetFile, IDocumentationSetConte
283296 }
284297 }
285298
299+ private static readonly HashSet < string > AllowedImageExtensions =
300+ [ ".svg" , ".png" , ".jpg" , ".jpeg" , ".gif" , ".webp" , ".ico" ] ;
301+
302+ private static BrandingConfiguration ValidateBranding ( BrandingConfiguration branding , IDocumentationSetContext context )
303+ {
304+ branding . Icon = ValidateBrandingImage ( branding . Icon , "branding.icon" , context ) ;
305+ branding . OgImage = ValidateBrandingImage ( branding . OgImage , "branding.og-image" , context ) ;
306+ return branding ;
307+ }
308+
309+ private static string ? ValidateBrandingImage ( string ? imagePath , string fieldName , IDocumentationSetContext context )
310+ {
311+ if ( string . IsNullOrEmpty ( imagePath ) )
312+ return null ;
313+
314+ var ext = Path . GetExtension ( imagePath ) . ToLowerInvariant ( ) ;
315+ if ( ! AllowedImageExtensions . Contains ( ext ) )
316+ {
317+ context . EmitError ( context . ConfigurationPath ,
318+ $ "'{ fieldName } ' has unsupported extension '{ ext } '. Allowed: { string . Join ( ", " , AllowedImageExtensions ) } ") ;
319+ return null ;
320+ }
321+
322+ var resolved = context . ReadFileSystem . FileInfo . New (
323+ Path . GetFullPath ( Path . Join ( context . DocumentationSourceDirectory . FullName , imagePath ) )
324+ ) ;
325+
326+ if ( ! resolved . IsSubPathOf ( context . DocumentationSourceDirectory ) )
327+ {
328+ context . EmitError ( context . ConfigurationPath ,
329+ $ "'{ fieldName } ' path '{ imagePath } ' escapes the documentation source directory.") ;
330+ return null ;
331+ }
332+
333+ if ( resolved . LinkTarget is not null )
334+ {
335+ context . EmitError ( context . ConfigurationPath ,
336+ $ "'{ fieldName } ' path '{ imagePath } ' is a symbolic link, which is not allowed for branding images.") ;
337+ return null ;
338+ }
339+
340+ if ( ! resolved . Exists )
341+ {
342+ context . EmitError ( context . ConfigurationPath , $ "'{ fieldName } ' file '{ imagePath } ' does not exist.") ;
343+ return null ;
344+ }
345+
346+ return imagePath ;
347+ }
348+
286349 private static CrossLinkEntry ? ParseCrossLinkEntry ( string raw , DocSetRegistry docsetRegistry , IFileInfo configPath , IDocumentationContext context )
287350 {
288351 DocSetRegistry entryRegistry ;
0 commit comments