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:
Phil Scott
2022-02-14 12:44:25 -05:00
committed by GitHub
parent 74a2e10ff0
commit c2da268129
147 changed files with 4112 additions and 6897 deletions

View File

@ -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&lt;string&gt;</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);
}
}
}

View File

@ -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
{

View File

@ -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);
}
}
}

View 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>&lt;", StringComparison.Ordinal) + 9;
var openParen = name.IndexOf("&gt;<wbr>(", StringComparison.Ordinal);
var end = name.LastIndexOf("&gt;<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("&lt;", "<wbr>&lt;")
.Replace("&gt;", "&gt;<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;
}
}

View File

@ -1,4 +1,4 @@
namespace Docs
namespace Docs.Extensions
{
public static class StringExtensions
{

View File

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
namespace Docs.Models

View 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()
};
}
}

View File

@ -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);
}))
};

View File

@ -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()
{

View File

@ -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);

View File

@ -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>";
}
}

View File

@ -1,6 +1,7 @@
using System.Collections.Generic;
using Statiq.Common;
using System.Xml.Linq;
using Docs.Extensions;
namespace Docs.Shortcodes
{

View File

@ -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.

View File

@ -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"));

View 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;
}
}

View File

@ -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()
{
}

View 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;
}
}

View 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>();
}

View File

@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;
namespace Docs.Utilities
{