mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-08-02 18:17:30 +08:00
Docs redesign (#728)
* Adding a dark mode * Adding reference for types to summary pages * Adding API Reference * Adding modifiers to methods/fields/etc * Minimizing files input * Caching a lot of the output pages * Cache only for each execution * Adding API references to existing docs
This commit is contained in:
@ -2,14 +2,26 @@ namespace Docs
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
public const string NoContainer = nameof(NoContainer);
|
||||
public const string NoSidebar = nameof(NoSidebar);
|
||||
public const string NoLink = nameof(NoLink);
|
||||
public const string Topic = nameof(Topic);
|
||||
public const string EditLink = nameof(EditLink);
|
||||
public const string Description = nameof(Description);
|
||||
public const string Hidden = nameof(Hidden);
|
||||
|
||||
/// <summary>
|
||||
/// Indicates where to locate source files for the API documentation.
|
||||
/// By default the globbing pattern "src/**/{!bin,!obj,!packages,!*.Tests,}/**/*.cs"
|
||||
/// is used which searches for all "*.cs" files at any depth under a "src" folder
|
||||
/// but not under "bin", "obj", "packages" or "Tests" folders. You can specify
|
||||
/// your own globbing pattern (or more than one globbing pattern) if your source
|
||||
/// files are found elsewhere.
|
||||
/// </summary>
|
||||
/// <type><see cref="string"/> or <c>IEnumerable<string></c></type>
|
||||
public const string SourceFiles = nameof(SourceFiles);
|
||||
|
||||
public const string ExampleSourceFiles = nameof(ExampleSourceFiles);
|
||||
|
||||
public const string ApiReference = "Reference";
|
||||
|
||||
public static class Emojis
|
||||
{
|
||||
public const string Root = "EMOJIS_ROOT";
|
||||
@ -27,18 +39,11 @@ namespace Docs
|
||||
public const string Repository = "SITE_REPOSITORY";
|
||||
public const string Branch = "SITE_BRANCH";
|
||||
}
|
||||
|
||||
|
||||
public static class Deployment
|
||||
{
|
||||
public const string GitHubToken = "GITHUB_TOKEN";
|
||||
public const string TargetBranch = "DEPLOYMENT_TARGET_BRANCH";
|
||||
}
|
||||
|
||||
public static class Sections
|
||||
{
|
||||
public const string Splash = nameof(Splash);
|
||||
public const string Sidebar = nameof(Sidebar);
|
||||
public const string Subtitle = nameof(Subtitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
using Statiq.App;
|
||||
using Statiq.Common;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Docs
|
||||
namespace Docs.Extensions
|
||||
{
|
||||
public static class BootstrapperExtensions
|
||||
{
|
||||
|
@ -1,8 +1,9 @@
|
||||
using Statiq.Common;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Statiq.CodeAnalysis;
|
||||
using Statiq.Common;
|
||||
|
||||
namespace Docs
|
||||
namespace Docs.Extensions
|
||||
{
|
||||
public static class DocumentExtensions
|
||||
{
|
||||
@ -25,5 +26,40 @@ namespace Docs
|
||||
{
|
||||
return source.Where(x => x.IsVisible());
|
||||
}
|
||||
|
||||
public static string GetModifiers(this IDocument document) => document.GetModifiers(false);
|
||||
|
||||
public static string GetModifiers(this IDocument document, bool skipStatic)
|
||||
{
|
||||
var modifiers = new List<string>();
|
||||
var accessibility = document.GetString(CodeAnalysisKeys.Accessibility).ToLower();
|
||||
if (accessibility != "public")
|
||||
{
|
||||
modifiers.Add(accessibility);
|
||||
}
|
||||
|
||||
// for some things, like ExtensionMethods, static will always be set.
|
||||
if (!skipStatic && document.GetBool(CodeAnalysisKeys.IsStatic))
|
||||
{
|
||||
modifiers.Add("static");
|
||||
}
|
||||
|
||||
if (document.GetBool(CodeAnalysisKeys.IsVirtual))
|
||||
{
|
||||
modifiers.Add("virtual");
|
||||
}
|
||||
|
||||
if (document.GetBool(CodeAnalysisKeys.IsAbstract))
|
||||
{
|
||||
modifiers.Add("abstract");
|
||||
}
|
||||
|
||||
if (document.GetBool(CodeAnalysisKeys.IsOverride))
|
||||
{
|
||||
modifiers.Add("override");
|
||||
}
|
||||
|
||||
return string.Join(' ', modifiers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
228
docs/src/Extensions/IExecutionContextExtensions.cs
Normal file
228
docs/src/Extensions/IExecutionContextExtensions.cs
Normal file
@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Docs.Pipelines;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Statiq.CodeAnalysis;
|
||||
using Statiq.Common;
|
||||
|
||||
namespace Docs.Extensions;
|
||||
|
||||
public static class IExecutionContextExtensions
|
||||
{
|
||||
private static readonly object _executionCacheLock = new();
|
||||
private static readonly ConcurrentDictionary<string, object> _executionCache = new();
|
||||
private static Guid _lastExecutionId = Guid.Empty;
|
||||
|
||||
public record SidebarItem(IDocument Node, string Title, bool ShowLink, ImmutableList<SidebarItem> Leafs);
|
||||
|
||||
public static bool TryGetCommentIdDocument(this IExecutionContext context, string commentId, out IDocument document,
|
||||
out string error)
|
||||
{
|
||||
context.ThrowIfNull(nameof(context));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(commentId))
|
||||
{
|
||||
document = default;
|
||||
error = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var documents = context.GetExecutionCache(nameof(TryGetCommentIdDocument), ctx => ctx.Outputs.FromPipeline(nameof(ExampleSyntax)).Flatten());
|
||||
|
||||
var matches = documents
|
||||
.Where(x => x.GetString(CodeAnalysisKeys.CommentId)?.Equals(commentId, StringComparison.OrdinalIgnoreCase) == true)
|
||||
.ToImmutableDocumentArray();
|
||||
|
||||
if (matches.Length == 1)
|
||||
{
|
||||
document = matches[0];
|
||||
error = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
document = default;
|
||||
error = matches.Length > 1
|
||||
? $"Multiple ambiguous matching documents found for commentId \"{commentId}\""
|
||||
: $"Couldn't find document with xref \"{commentId}\"";
|
||||
return false;
|
||||
}
|
||||
|
||||
public static T GetExecutionCache<T>(this IExecutionContext context, string key, Func<IExecutionContext, T> getter)
|
||||
{
|
||||
lock (_executionCacheLock)
|
||||
{
|
||||
if (_lastExecutionId != context.ExecutionId)
|
||||
{
|
||||
_executionCache.Clear();
|
||||
_lastExecutionId = context.ExecutionId;
|
||||
}
|
||||
|
||||
return (T)_executionCache.GetOrAdd(key, valueFactory: _ => getter.Invoke(context));
|
||||
}
|
||||
}
|
||||
|
||||
public static NormalizedPath FindCard(this IExecutionContext context, Guid docId)
|
||||
{
|
||||
var cardLookups = context.GetExecutionCache(nameof(FindCard), ctx =>
|
||||
{
|
||||
return ctx.Outputs
|
||||
.Select(i => new { DocId = i.GetString("DocId"), Destination = i.Destination })
|
||||
.Where(i => i.DocId != null)
|
||||
.ToDictionary(i => i.DocId, i => i.Destination);
|
||||
});
|
||||
|
||||
return !cardLookups.ContainsKey(docId.ToString()) ? null : cardLookups[docId.ToString()];
|
||||
}
|
||||
|
||||
public static SidebarItem GetSidebar(this IExecutionContext context)
|
||||
{
|
||||
return context.GetExecutionCache(nameof(GetSidebar), ctx =>
|
||||
{
|
||||
var outputPages = ctx.OutputPages;
|
||||
var root = outputPages["index.html"][0];
|
||||
var children = outputPages
|
||||
.GetChildrenOf(root)
|
||||
.OrderBy(i => i.GetInt("Order"))
|
||||
.OnlyVisible().Select(child =>
|
||||
{
|
||||
var showLink = child.ShowLink();
|
||||
var children = outputPages
|
||||
.GetChildrenOf(child)
|
||||
.OnlyVisible()
|
||||
.Select(subChild =>
|
||||
new SidebarItem(subChild, subChild.GetTitle(), true, ImmutableList<SidebarItem>.Empty))
|
||||
.ToImmutableList();
|
||||
|
||||
return new SidebarItem(child, child.GetTitle(), showLink, children);
|
||||
}).ToImmutableList();
|
||||
|
||||
return new SidebarItem(root, root.GetTitle(), false, children);
|
||||
});
|
||||
}
|
||||
|
||||
public static HtmlString GetTypeLink(this IExecutionContext context, IDocument document) =>
|
||||
context.GetTypeLink(document, null, true);
|
||||
|
||||
public static HtmlString GetTypeLink(this IExecutionContext context, IDocument document, bool linkTypeArguments) =>
|
||||
context.GetTypeLink(document, null, linkTypeArguments);
|
||||
|
||||
public static HtmlString GetTypeLink(this IExecutionContext context, IDocument document, string name) =>
|
||||
context.GetTypeLink(document, name, true);
|
||||
|
||||
public static HtmlString GetTypeLink(this IExecutionContext context, IDocument document, string name,
|
||||
bool linkTypeArguments)
|
||||
{
|
||||
name ??= document.GetString(CodeAnalysisKeys.DisplayName);
|
||||
|
||||
// Link nullable types to their type argument
|
||||
if (document.GetString(CodeAnalysisKeys.Name) == "Nullable")
|
||||
{
|
||||
var nullableType = document.GetDocumentList(CodeAnalysisKeys.TypeArguments)?.FirstOrDefault();
|
||||
if (nullableType != null)
|
||||
{
|
||||
return context.GetTypeLink(nullableType, name);
|
||||
}
|
||||
}
|
||||
|
||||
// If it wasn't nullable, format the name
|
||||
name = context.GetFormattedHtmlName(name);
|
||||
|
||||
// Link the type and type parameters separately for generic types
|
||||
IReadOnlyList<IDocument> typeArguments = document.GetDocumentList(CodeAnalysisKeys.TypeArguments);
|
||||
if (typeArguments?.Count > 0)
|
||||
{
|
||||
// Link to the original definition of the generic type
|
||||
document = document.GetDocument(CodeAnalysisKeys.OriginalDefinition) ?? document;
|
||||
|
||||
if (linkTypeArguments)
|
||||
{
|
||||
// Get the type argument positions
|
||||
var begin = name.IndexOf("<wbr><", StringComparison.Ordinal) + 9;
|
||||
var openParen = name.IndexOf("><wbr>(", StringComparison.Ordinal);
|
||||
var end = name.LastIndexOf("><wbr>", openParen == -1 ? name.Length : openParen,
|
||||
StringComparison.Ordinal); // Don't look past the opening paren if there is one
|
||||
|
||||
if (begin == -1 || end == -1)
|
||||
{
|
||||
return new HtmlString(name);
|
||||
}
|
||||
|
||||
// Remove existing type arguments and insert linked type arguments (do this first to preserve original indexes)
|
||||
name = name
|
||||
.Remove(begin, end - begin)
|
||||
.Insert(begin,
|
||||
string.Join(", <wbr>", typeArguments.Select(x => context.GetTypeLink(x, true).Value)));
|
||||
|
||||
// Insert the link for the type
|
||||
if (!document.Destination.IsNullOrEmpty)
|
||||
{
|
||||
name = name.Insert(begin - 9, "</a>").Insert(0, $"<a href=\"{context.GetLink(document)}\">");
|
||||
}
|
||||
|
||||
return new HtmlString(name);
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a type parameter, create an anchor link to the declaring type's original definition
|
||||
if (document.GetString(CodeAnalysisKeys.Kind) == "TypeParameter")
|
||||
{
|
||||
var declaringType = document.GetDocument(CodeAnalysisKeys.DeclaringType)
|
||||
?.GetDocument(CodeAnalysisKeys.OriginalDefinition);
|
||||
if (declaringType != null)
|
||||
{
|
||||
return new HtmlString(declaringType.Destination.IsNullOrEmpty
|
||||
? name
|
||||
: $"<a href=\"{context.GetLink(declaringType)}#typeparam-{document["Name"]}\">{name}</a>");
|
||||
}
|
||||
}
|
||||
|
||||
return new HtmlString(document.Destination.IsNullOrEmpty
|
||||
? name
|
||||
: $"<a href=\"{context.GetLink(document)}\">{name}</a>");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats a symbol or other name by encoding HTML characters and
|
||||
/// adding HTML break elements as appropriate.
|
||||
/// </summary>
|
||||
/// <param name="context">The execution context.</param>
|
||||
/// <param name="name">The name to format.</param>
|
||||
/// <returns>The name formatted for use in HTML.</returns>
|
||||
public static string GetFormattedHtmlName(this IExecutionContext context, string name)
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Encode and replace .()<> with word break opportunities
|
||||
name = WebUtility.HtmlEncode(name)
|
||||
.Replace(".", "<wbr>.")
|
||||
.Replace("(", "<wbr>(")
|
||||
.Replace(")", ")<wbr>")
|
||||
.Replace(", ", ", <wbr>")
|
||||
.Replace("<", "<wbr><")
|
||||
.Replace(">", "><wbr>");
|
||||
|
||||
// Add additional break opportunities in long un-broken segments
|
||||
var segments = name.Split(new[] { "<wbr>" }, StringSplitOptions.None).ToList();
|
||||
var replaced = false;
|
||||
for (var c = 0; c < segments.Count; c++)
|
||||
{
|
||||
if (segments[c].Length > 20)
|
||||
{
|
||||
segments[c] = new string(segments[c]
|
||||
.SelectMany(
|
||||
(x, i) => char.IsUpper(x) && i != 0 ? new[] { '<', 'w', 'b', 'r', '>', x } : new[] { x })
|
||||
.ToArray());
|
||||
replaced = true;
|
||||
}
|
||||
}
|
||||
|
||||
return replaced ? string.Join("<wbr>", segments) : name;
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
namespace Docs
|
||||
namespace Docs.Extensions
|
||||
{
|
||||
public static class StringExtensions
|
||||
{
|
||||
|
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Docs.Models
|
||||
|
151
docs/src/Pipelines/CodePipeline.cs
Normal file
151
docs/src/Pipelines/CodePipeline.cs
Normal file
@ -0,0 +1,151 @@
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Docs.Utilities;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Statiq.CodeAnalysis;
|
||||
using Statiq.Common;
|
||||
using Statiq.Core;
|
||||
using Statiq.Web;
|
||||
using Statiq.Web.Pipelines;
|
||||
|
||||
namespace Docs.Pipelines;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Loads source files.
|
||||
/// </summary>
|
||||
public class Code : Pipeline
|
||||
{
|
||||
public Code()
|
||||
{
|
||||
InputModules = new ModuleList(
|
||||
new ReadFiles(
|
||||
Config.FromSettings(settings
|
||||
=> settings.GetList<string>(Constants.SourceFiles).AsEnumerable())));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads source files.
|
||||
/// </summary>
|
||||
public class ExampleCode : Pipeline
|
||||
{
|
||||
public ExampleCode()
|
||||
{
|
||||
Dependencies.Add(nameof(Code));
|
||||
|
||||
InputModules = new ModuleList(
|
||||
new ReadFiles(
|
||||
Config.FromSettings(settings
|
||||
=> settings.GetList<string>(Constants.ExampleSourceFiles).AsEnumerable())));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses Roslyn to analyze any source files loaded in the previous
|
||||
/// pipeline along with any specified assemblies. This pipeline
|
||||
/// results in documents that represent Roslyn symbols.
|
||||
/// </summary>
|
||||
public class ExampleSyntax : Pipeline
|
||||
{
|
||||
public ExampleSyntax()
|
||||
{
|
||||
Dependencies.Add(nameof(ExampleCode));
|
||||
DependencyOf.Add(nameof(Content));
|
||||
|
||||
ProcessModules = new ModuleList
|
||||
{
|
||||
new ConcatDocuments(nameof(Code)),
|
||||
new ConcatDocuments(nameof(ExampleCode)),
|
||||
new CacheDocuments(
|
||||
new AnalyzeCSharp()
|
||||
.WhereNamespaces(true)
|
||||
.WherePublic()
|
||||
.WithCssClasses("code", "cs")
|
||||
.WithDestinationPrefix("syntax")
|
||||
.WithAssemblySymbols()
|
||||
// we need to load Spectre.Console for compiling, but we don't need to process it in Statiq
|
||||
.WhereNamespaces(i => !i.StartsWith("Spectre.Console"))
|
||||
.WithImplicitInheritDoc(false),
|
||||
new ExecuteConfig(Config.FromDocument((doc, _) =>
|
||||
{
|
||||
// Add metadata
|
||||
var metadataItems = new MetadataItems
|
||||
{
|
||||
// Calculate an xref that includes a "api-" prefix to avoid collisions
|
||||
{ WebKeys.Xref, "syntax-" + doc.GetString(CodeAnalysisKeys.CommentId) },
|
||||
};
|
||||
|
||||
var contentProvider = doc.ContentProvider;
|
||||
return doc.Clone(metadataItems, contentProvider);
|
||||
}))).WithoutSourceMapping()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates API documentation pipeline.
|
||||
/// </summary>
|
||||
public class Api : Pipeline
|
||||
{
|
||||
public Api()
|
||||
{
|
||||
Dependencies.Add(nameof(Code));
|
||||
DependencyOf.Add(nameof(Content));
|
||||
|
||||
ProcessModules = new ModuleList
|
||||
{
|
||||
new ConcatDocuments(nameof(Code)),
|
||||
new CacheDocuments(
|
||||
new AnalyzeCSharp()
|
||||
.WhereNamespaces(ns => ns.StartsWith("Spectre.Console") && !ns.Contains("Analyzer") &&
|
||||
!ns.Contains("Testing") && !ns.Contains("Examples"))
|
||||
.WherePublic(true)
|
||||
.WithCssClasses("code", "cs")
|
||||
.WithDestinationPrefix("api")
|
||||
.WithAssemblySymbols()
|
||||
.WithImplicitInheritDoc(false),
|
||||
new ExecuteConfig(Config.FromDocument((doc, ctx) =>
|
||||
{
|
||||
// Calculate a type name to link lookup for auto linking
|
||||
string name = null;
|
||||
|
||||
var kind = doc.GetString(CodeAnalysisKeys.Kind);
|
||||
switch (kind)
|
||||
{
|
||||
case "NamedType":
|
||||
name = doc.GetString(CodeAnalysisKeys.DisplayName);
|
||||
break;
|
||||
case "Method":
|
||||
var containingType = doc.GetDocument(CodeAnalysisKeys.ContainingType);
|
||||
if (containingType != null)
|
||||
{
|
||||
name =
|
||||
$"{containingType.GetString(CodeAnalysisKeys.DisplayName)}.{doc.GetString(CodeAnalysisKeys.DisplayName)}";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (name != null)
|
||||
{
|
||||
var typeNameLinks = ctx.GetRequiredService<TypeNameLinks>();
|
||||
typeNameLinks.Links.AddOrUpdate(WebUtility.HtmlEncode(name), ctx.GetLink(doc),
|
||||
(_, _) => string.Empty);
|
||||
}
|
||||
|
||||
// Add metadata
|
||||
var metadataItems = new MetadataItems
|
||||
{
|
||||
{ WebKeys.Xref, doc.GetString(CodeAnalysisKeys.CommentId) },
|
||||
{ WebKeys.Layout, "api/_layout.cshtml" },
|
||||
{ Constants.Hidden, true }
|
||||
};
|
||||
|
||||
var contentProvider = doc.ContentProvider.CloneWithMediaType(MediaTypes.Html);
|
||||
metadataItems.Add(WebKeys.ContentType, ContentType.Content);
|
||||
return doc.Clone(metadataItems, contentProvider);
|
||||
}))).WithoutSourceMapping()
|
||||
};
|
||||
}
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Docs.Models;
|
||||
using Statiq.Common;
|
||||
@ -14,7 +12,7 @@ namespace Docs.Pipelines
|
||||
InputModules = new ModuleList
|
||||
{
|
||||
new ExecuteConfig(
|
||||
Config.FromContext(ctx => {
|
||||
Config.FromContext(_ => {
|
||||
return new ReadWeb(Constants.Colors.Url);
|
||||
}))
|
||||
};
|
||||
@ -22,9 +20,9 @@ namespace Docs.Pipelines
|
||||
ProcessModules = new ModuleList
|
||||
{
|
||||
new ExecuteConfig(
|
||||
Config.FromDocument(async (doc, ctx) =>
|
||||
Config.FromDocument(async (doc, _) =>
|
||||
{
|
||||
var data = Color.Parse(await doc.GetContentStringAsync()).ToList();
|
||||
var data = Color.Parse(await doc.GetContentStringAsync()).ToList();
|
||||
return data.ToDocument(Constants.Colors.Root);
|
||||
}))
|
||||
};
|
||||
|
@ -1,10 +1,9 @@
|
||||
using Statiq.Common;
|
||||
using Statiq.Core;
|
||||
using Statiq.Web.GitHub;
|
||||
|
||||
namespace Docs.Pipelines
|
||||
{
|
||||
public class DeploymentPipeline : Pipeline
|
||||
public class DeploymentPipeline : Statiq.Core.Pipeline
|
||||
{
|
||||
public DeploymentPipeline()
|
||||
{
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System.Collections.Generic;
|
||||
using Docs.Models;
|
||||
using Docs.Modules;
|
||||
using Statiq.Common;
|
||||
@ -13,7 +12,7 @@ namespace Docs.Pipelines
|
||||
InputModules = new ModuleList
|
||||
{
|
||||
new ExecuteConfig(
|
||||
Config.FromContext(ctx => {
|
||||
Config.FromContext(_ => {
|
||||
return new ReadEmbedded(
|
||||
typeof(EmojiPipeline).Assembly,
|
||||
"Docs/src/Data/emojis.json");
|
||||
@ -23,7 +22,7 @@ namespace Docs.Pipelines
|
||||
ProcessModules = new ModuleList
|
||||
{
|
||||
new ExecuteConfig(
|
||||
Config.FromDocument(async (doc, ctx) =>
|
||||
Config.FromDocument(async (doc, _) =>
|
||||
{
|
||||
var data = Emoji.Parse(await doc.GetContentStringAsync());
|
||||
return data.ToDocument(Constants.Emojis.Root);
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Statiq.Common;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Docs.Shortcodes
|
||||
{
|
||||
@ -8,7 +7,6 @@ namespace Docs.Shortcodes
|
||||
{
|
||||
public override ShortcodeResult Execute(KeyValuePair<string, string>[] args, string content, IDocument document, IExecutionContext context)
|
||||
{
|
||||
|
||||
return $"<div class=\"alert-warning\">{content}</div>";
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Statiq.Common;
|
||||
using System.Xml.Linq;
|
||||
using Docs.Extensions;
|
||||
|
||||
namespace Docs.Shortcodes
|
||||
{
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Statiq.Common;
|
||||
@ -10,8 +9,6 @@ namespace Docs.Shortcodes
|
||||
{
|
||||
public class ColorTableShortcode : SyncShortcode
|
||||
{
|
||||
private const string ColorStyle = "display: inline-block;width: 60px; height: 15px;";
|
||||
|
||||
public override ShortcodeResult Execute(KeyValuePair<string, string>[] args, string content, IDocument document, IExecutionContext context)
|
||||
{
|
||||
// Get the definition.
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Statiq.Common;
|
||||
@ -31,7 +30,6 @@ namespace Docs.Shortcodes
|
||||
|
||||
foreach (var emoji in emojis)
|
||||
{
|
||||
var code = emoji.Code.Replace("U+0000", "U+").Replace("U+000", "U+");
|
||||
var icon = $"&#x{emoji.Code.Replace("U+", string.Empty)};";
|
||||
|
||||
var row = new XElement("tr", new XAttribute("class", "search-row"));
|
||||
|
43
docs/src/Shortcodes/ExampleSnippet.cs
Normal file
43
docs/src/Shortcodes/ExampleSnippet.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Docs.Extensions;
|
||||
using Docs.Utilities;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Statiq.CodeAnalysis;
|
||||
using Statiq.Common;
|
||||
|
||||
namespace Docs.Shortcodes;
|
||||
|
||||
public class ExampleSnippet : Shortcode
|
||||
{
|
||||
protected const string Solution = nameof(Solution);
|
||||
protected const string Project = nameof(Project);
|
||||
protected const string Symbol = nameof(Symbol);
|
||||
protected const string BodyOnly = nameof(BodyOnly);
|
||||
|
||||
public override async Task<ShortcodeResult> ExecuteAsync(KeyValuePair<string, string>[] args, string content,
|
||||
IDocument document, IExecutionContext context)
|
||||
{
|
||||
var props = args.ToDictionary(Solution, Project, Symbol, BodyOnly);
|
||||
var symbolName = props.GetString(Symbol);
|
||||
var bodyOnly = props.Get<bool?>(BodyOnly) ?? symbolName.StartsWith("m:", StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
if (!context.TryGetCommentIdDocument(symbolName, out var apiDocument, out _))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var options = HighlightService.HighlightOption.All;
|
||||
if (bodyOnly)
|
||||
{
|
||||
options = HighlightService.HighlightOption.Body;
|
||||
}
|
||||
|
||||
var comp = apiDocument.Get<Compilation>(CodeAnalysisKeys.Compilation);
|
||||
var symbol = apiDocument.Get<ISymbol>(CodeAnalysisKeys.Symbol);
|
||||
var highlightElement = await HighlightService.Highlight(comp, symbol, options);
|
||||
ShortcodeResult shortcodeResult = $"<pre><code>{highlightElement}</code></pre>";
|
||||
return shortcodeResult;
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Docs.SocialCards{
|
||||
public class SocialCardModel : PageModel
|
||||
@ -17,13 +16,6 @@ namespace Docs.SocialCards{
|
||||
[BindProperty(Name = "footer", SupportsGet = true)]
|
||||
public string Footer { get; set; }
|
||||
|
||||
private readonly ILogger<SocialCardModel> _logger;
|
||||
|
||||
public SocialCardModel(ILogger<SocialCardModel> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
|
234
docs/src/Utilities/HighlightService.cs
Normal file
234
docs/src/Utilities/HighlightService.cs
Normal file
@ -0,0 +1,234 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Classification;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace Docs.Utilities;
|
||||
|
||||
internal static class HighlightService
|
||||
{
|
||||
internal enum HighlightOption
|
||||
{
|
||||
All,
|
||||
Body
|
||||
}
|
||||
|
||||
private static readonly AdhocWorkspace _emptyWorkspace = new();
|
||||
|
||||
public static async Task<string> Highlight(Compilation compilation, ISymbol symbol, HighlightOption option = HighlightOption.All)
|
||||
{
|
||||
var syntaxReference = symbol.DeclaringSyntaxReferences.FirstOrDefault();
|
||||
if (syntaxReference == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var syntax = await syntaxReference.GetSyntaxAsync();
|
||||
var indent = GetIndent(syntax.GetLeadingTrivia());
|
||||
var model = compilation.GetSemanticModel(syntaxReference.SyntaxTree);
|
||||
|
||||
var methodWithBodySyntax = syntax as BaseMethodDeclarationSyntax;
|
||||
|
||||
TextSpan textSpan;
|
||||
switch (option)
|
||||
{
|
||||
case HighlightOption.Body when methodWithBodySyntax is { Body: { } }:
|
||||
{
|
||||
syntax = methodWithBodySyntax.Body;
|
||||
indent = GetIndent(methodWithBodySyntax.Body.Statements.First().GetLeadingTrivia());
|
||||
textSpan = TextSpan.FromBounds(syntax.Span.Start + 1, syntax.Span.End - 1);
|
||||
break;
|
||||
}
|
||||
case HighlightOption.Body when methodWithBodySyntax is { ExpressionBody: { } }:
|
||||
{
|
||||
syntax = methodWithBodySyntax.ExpressionBody;
|
||||
textSpan = syntax.Span;
|
||||
break;
|
||||
}
|
||||
case HighlightOption.All:
|
||||
default:
|
||||
textSpan = syntax.Span;
|
||||
break;
|
||||
}
|
||||
|
||||
var text = await syntaxReference.SyntaxTree.GetTextAsync();
|
||||
// we need a workspace, but it seems it is only used to resolve a few services and nothing else so an empty one will suffice
|
||||
return HighlightElement(_emptyWorkspace, model, text, textSpan, indent);
|
||||
}
|
||||
|
||||
private static int GetIndent(SyntaxTriviaList leadingTrivia)
|
||||
{
|
||||
var whitespace = leadingTrivia.FirstOrDefault(i => i.Kind() == SyntaxKind.WhitespaceTrivia);
|
||||
return whitespace == default ? 0 : whitespace.Span.Length;
|
||||
}
|
||||
|
||||
private static string HighlightElement(Workspace workspace, SemanticModel semanticModel, SourceText fullSourceText,
|
||||
TextSpan textSpan, int indent)
|
||||
{
|
||||
|
||||
var classifiedSpans = Classifier.GetClassifiedSpans(semanticModel, textSpan, workspace);
|
||||
return HighlightElement(classifiedSpans, fullSourceText, indent);
|
||||
}
|
||||
|
||||
private static string HighlightElement(IEnumerable<ClassifiedSpan> classifiedSpans, SourceText fullSourceText, int indent)
|
||||
{
|
||||
|
||||
var ranges = classifiedSpans.Select(classifiedSpan =>
|
||||
new Range(classifiedSpan.ClassificationType, classifiedSpan.TextSpan, fullSourceText)).ToList();
|
||||
|
||||
// the classified text won't include the whitespace so we need to add to fill in those gaps.
|
||||
ranges = FillGaps(fullSourceText, ranges).ToList();
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
var cssClass = ClassificationTypeToPrismClass(range.ClassificationType);
|
||||
if (string.IsNullOrWhiteSpace(cssClass))
|
||||
{
|
||||
sb.Append(range.Text);
|
||||
}
|
||||
else
|
||||
{
|
||||
// include the prism css class but also include the roslyn classification.
|
||||
sb.Append(
|
||||
$"<span class=\"token {cssClass} roslyn-{range.ClassificationType.Replace(" ", "-")}\">{range.Text}</span>");
|
||||
}
|
||||
}
|
||||
|
||||
// there might be a way to do this with roslyn, but for now we'll just normalize everything off of the length of the
|
||||
// leading trivia of the element we are looking at.
|
||||
var indentString = new string(' ', indent);
|
||||
var allLines = sb.ToString()
|
||||
.ReplaceLineEndings()
|
||||
.Split(Environment.NewLine)
|
||||
.Select(i => i.StartsWith(indentString) == false ? i : i[indent..]);
|
||||
|
||||
return string.Join(Environment.NewLine, allLines);
|
||||
}
|
||||
|
||||
private static string ClassificationTypeToPrismClass(string rangeClassificationType)
|
||||
{
|
||||
if (rangeClassificationType == null)
|
||||
return string.Empty;
|
||||
|
||||
switch (rangeClassificationType)
|
||||
{
|
||||
case ClassificationTypeNames.Identifier:
|
||||
return "symbol";
|
||||
case ClassificationTypeNames.LocalName:
|
||||
return "variable";
|
||||
case ClassificationTypeNames.ParameterName:
|
||||
case ClassificationTypeNames.PropertyName:
|
||||
case ClassificationTypeNames.EnumMemberName:
|
||||
case ClassificationTypeNames.FieldName:
|
||||
return "property";
|
||||
case ClassificationTypeNames.ClassName:
|
||||
case ClassificationTypeNames.StructName:
|
||||
case ClassificationTypeNames.RecordClassName:
|
||||
case ClassificationTypeNames.RecordStructName:
|
||||
case ClassificationTypeNames.InterfaceName:
|
||||
case ClassificationTypeNames.DelegateName:
|
||||
case ClassificationTypeNames.EnumName:
|
||||
case ClassificationTypeNames.ModuleName:
|
||||
case ClassificationTypeNames.TypeParameterName:
|
||||
return "title.class";
|
||||
case ClassificationTypeNames.MethodName:
|
||||
case ClassificationTypeNames.ExtensionMethodName:
|
||||
return "title.function";
|
||||
case ClassificationTypeNames.Comment:
|
||||
return "comment";
|
||||
case ClassificationTypeNames.Keyword:
|
||||
case ClassificationTypeNames.ControlKeyword:
|
||||
case ClassificationTypeNames.PreprocessorKeyword:
|
||||
return "keyword";
|
||||
case ClassificationTypeNames.StringLiteral:
|
||||
case ClassificationTypeNames.VerbatimStringLiteral:
|
||||
return "string";
|
||||
case ClassificationTypeNames.NumericLiteral:
|
||||
return "number";
|
||||
case ClassificationTypeNames.Operator:
|
||||
case ClassificationTypeNames.StringEscapeCharacter:
|
||||
return "operator";
|
||||
case ClassificationTypeNames.Punctuation:
|
||||
return "punctuation";
|
||||
case ClassificationTypeNames.StaticSymbol:
|
||||
return string.Empty;
|
||||
case ClassificationTypeNames.XmlDocCommentComment:
|
||||
case ClassificationTypeNames.XmlDocCommentDelimiter:
|
||||
case ClassificationTypeNames.XmlDocCommentName:
|
||||
case ClassificationTypeNames.XmlDocCommentText:
|
||||
case ClassificationTypeNames.XmlDocCommentAttributeName:
|
||||
case ClassificationTypeNames.XmlDocCommentAttributeQuotes:
|
||||
case ClassificationTypeNames.XmlDocCommentAttributeValue:
|
||||
case ClassificationTypeNames.XmlDocCommentEntityReference:
|
||||
case ClassificationTypeNames.XmlDocCommentProcessingInstruction:
|
||||
case ClassificationTypeNames.XmlDocCommentCDataSection:
|
||||
return "comment";
|
||||
default:
|
||||
return rangeClassificationType.Replace(" ", "-");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<Range> FillGaps(SourceText text, IList<Range> ranges)
|
||||
{
|
||||
const string WhitespaceClassification = null;
|
||||
var current = ranges.First().TextSpan.Start;
|
||||
var end = ranges.Last().TextSpan.End;
|
||||
Range previous = null;
|
||||
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
var start = range.TextSpan.Start;
|
||||
if (start > current)
|
||||
{
|
||||
yield return new Range(WhitespaceClassification, TextSpan.FromBounds(current, start), text);
|
||||
}
|
||||
|
||||
if (previous == null || range.TextSpan != previous.TextSpan)
|
||||
{
|
||||
yield return range;
|
||||
}
|
||||
|
||||
previous = range;
|
||||
current = range.TextSpan.End;
|
||||
}
|
||||
|
||||
if (current < end)
|
||||
{
|
||||
yield return new Range(WhitespaceClassification, TextSpan.FromBounds(current, end), text);
|
||||
}
|
||||
}
|
||||
|
||||
private class Range
|
||||
{
|
||||
private ClassifiedSpan ClassifiedSpan { get; }
|
||||
public string Text { get; }
|
||||
|
||||
public Range(string classification, TextSpan span, SourceText text) :
|
||||
this(classification, span, text.GetSubText(span).ToString())
|
||||
{
|
||||
}
|
||||
|
||||
private Range(string classification, TextSpan span, string text) :
|
||||
this(new ClassifiedSpan(classification, span), text)
|
||||
{
|
||||
}
|
||||
|
||||
private Range(ClassifiedSpan classifiedSpan, string text)
|
||||
{
|
||||
ClassifiedSpan = classifiedSpan;
|
||||
Text = text;
|
||||
}
|
||||
|
||||
public string ClassificationType => ClassifiedSpan.ClassificationType;
|
||||
|
||||
public TextSpan TextSpan => ClassifiedSpan.TextSpan;
|
||||
}
|
||||
}
|
8
docs/src/Utilities/TypeNameLinks.cs
Normal file
8
docs/src/Utilities/TypeNameLinks.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Docs.Utilities;
|
||||
|
||||
public class TypeNameLinks
|
||||
{
|
||||
public ConcurrentDictionary<string, string> Links { get; } = new ConcurrentDictionary<string, string>();
|
||||
}
|
@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace Docs.Utilities
|
||||
{
|
||||
|
Reference in New Issue
Block a user