Skip to content

Commit

Permalink
Merge pull request #60 from YumeChan-DT/feature/docs
Browse files Browse the repository at this point in the history
Implement MoltenObsidian-flavoured plugin documentation
  • Loading branch information
SakuraIsayeki committed Apr 29, 2023
2 parents d37b96c + 3c930cf commit 3795d82
Show file tree
Hide file tree
Showing 19 changed files with 519 additions and 24 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/dotnet-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ jobs:
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
submodules: 'recursive'

- name: Setup .NET
Expand All @@ -28,6 +29,7 @@ jobs:
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
submodules: 'recursive'

- name: Setup .NET
Expand Down
17 changes: 17 additions & 0 deletions .run/YumeChan NetRunner.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="YumeChan NetRunner" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/src/YumeChan.NetRunner/YumeChan.NetRunner.csproj" />
<option name="LAUNCH_PROFILE_TFM" value="net6.0" />
<option name="LAUNCH_PROFILE_NAME" value="YumeChan.NetRunner" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
<option name="SEND_DEBUG_REQUEST" value="1" />
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Nodsoft.MoltenObsidian.Blazor;
using Nodsoft.MoltenObsidian.Blazor.Services;
using Nodsoft.MoltenObsidian.Vault;
using Swashbuckle.AspNetCore.SwaggerGen;
using YumeChan.NetRunner.Plugins.Infrastructure.Api;
using YumeChan.NetRunner.Plugins.Infrastructure.Swagger;
using YumeChan.NetRunner.Plugins.Services;
using YumeChan.NetRunner.Plugins.Services.Docs;

namespace YumeChan.NetRunner.Plugins.Infrastructure;

public static class ApiPluginDependencyExtensions
public static class PluginSupportDependencyExtensions
{
public static IServiceCollection AddApiPluginSupport(this IServiceCollection services)
{
Expand Down Expand Up @@ -118,4 +122,18 @@ public static bool TryIncludeXmlCommentsFromAssembly(this SwaggerGenOptions opti

return false;
}

/// <summary>
/// Adds support for MoltenObsidian-flavoured plugin documentation, via the use of <see creef="PluginDocsLoader" />.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> to add the services to.</param>
/// <returns>The <see cref="IServiceCollection" /> so that additional calls can be chained.</returns>
public static IServiceCollection AddPluginDocsSupport(this IServiceCollection services)
{
services.AddMoltenObsidianBlazorIntegration();
services.AddSingleton<PluginDocsLoader>();
services.AddSingleton<VaultRouter>();

return services;
}
}
76 changes: 76 additions & 0 deletions src/YumeChan.NetRunner.Plugins/Services/Docs/PluginDocsLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Reflection;
using Nodsoft.MoltenObsidian.Vault;
using Nodsoft.MoltenObsidian.Vaults.FileSystem;
using YumeChan.Core.Config;
using YumeChan.Core.Services.Plugins;
using YumeChan.PluginBase;
using YumeChan.PluginBase.Infrastructure;

namespace YumeChan.NetRunner.Plugins.Services.Docs;

/// <summary>
/// <p>Provides a service to load a plugin's documentation.</p>
///
/// <p>
/// Documentation is loaded from the plugin's directory,
/// and is expected to be in the form of a single or multiple Markdown files.
///
/// A Tree structure is created from the Markdown files found recursively in the plugin's <c>/docs</c> directory.
/// </p>
///
/// </summary>
public sealed class PluginDocsLoader
{
private readonly ICoreProperties _coreProperties;
private readonly PluginsLoader _pluginsLoader;

public PluginDocsLoader(ICoreProperties coreProperties, PluginsLoader pluginsLoader)
{
_coreProperties = coreProperties;
_pluginsLoader = pluginsLoader;
}

private readonly Dictionary<string, IVault?> _vaults = new();

/// <summary>
/// Gets a <see cref="IVault"/> for the specified plugin.
/// </summary>
/// <param name="pluginName">The internal name of the plugin to get the vault for.</param>
/// <returns>The vault for the specified plugin.</returns>
/// <exception cref="ArgumentException">Thrown if the specified plugin does not exist.</exception>
public IVault? GetVault(string pluginName)
{
// First get from cache.
if (_vaults.TryGetValue(pluginName, out IVault? vault))
{
return vault;
}

// Then try to instantiate from plugin's assets.

// First. Does this plugin even exist?
if (!_pluginsLoader.PluginManifests.TryGetValue(pluginName, out _))
{
throw new ArgumentException($"Plugin '{pluginName}' does not exist.", nameof(pluginName));
}

// Then, do we have a PluginDocsAttribute?
Type pluginType = pluginName.GetType();

PluginDocsAttribute? attribute = pluginType.GetCustomAttributes<PluginDocsAttribute>().FirstOrDefault()
?? pluginType.Assembly.GetCustomAttributes<PluginDocsAttribute>().FirstOrDefault();

if (attribute is { Enabled: false })
{
// No, we don't. Cache the result and return null.
return _vaults[pluginName] = null;
}

// Yes, we do. Create a new FileSystemVault.
// Get the path to the plugin's docs directory.
string pluginDirectory = Path.Join(_coreProperties.Path_Plugins, pluginName, attribute?.Path ?? PluginDocsAttribute.DefaultPath);

// Create a new FileSystemVault, cache it, and return it.
return _vaults[pluginName] = FileSystemVault.FromDirectory(new(pluginDirectory));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
<PackageReference Include="Nodsoft.MoltenObsidian.Blazor" Version="0.4.3" />
<PackageReference Include="Nodsoft.MoltenObsidian.Vaults.FileSystem" Version="0.4.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

Expand Down
147 changes: 147 additions & 0 deletions src/YumeChan.NetRunner/Pages/Docs/DocsBrowser.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
@page "/docs"
@page "/docs/{PluginName}/{*PathSlug}"
@layout DocsLayout

@using YumeChan.NetRunner.Plugins.Services.Docs
@using Nodsoft.MoltenObsidian.Vault
@using Nodsoft.MoltenObsidian.Blazor
@using Nodsoft.MoltenObsidian.Blazor.Helpers
@using Nodsoft.MoltenObsidian.Blazor.Templates
@using YumeChan.NetRunner.Shared.Docs
@using System.IO

@inject PluginDocsLoader PluginDocsLoader

<div class="page mt-xl-3">
<DocsSidebar Vault="@_vault" SelectedPlugin="@PluginName" />

<main>
<article class="content px-4 my-3">
@BuildBreadcrumb(PathSlug)

@if (PluginName is null or "" && PathSlug is null or "")
{

}
@* Loading *@
else if (_vault is null && _loadingError is null)
{
<h3 class="text-info">Loading...</h3>
}
@* No docs found *@
else if (_loadingError is DirectoryNotFoundException || _vault?.Files.Count is 0)
{
<h3 class="text-warning">No docs found.</h3>
}
@* Error handling *@
else if (_loadingError is not null)
{
<h3 class="text-danger">Error loading plugin docs: <small>@_loadingError.Message</small></h3>
}
else
{
<ObsidianVaultDisplay @ref="@_vaultDisplay" Vault="@_vault" BasePath="@_basePath" CurrentPath="@PathSlug">
<NotFound>
<h3 class="text-warning">Not Found</h3>
</NotFound>

<FoundIndexNote>@FoundNote.Render(new(context.Note, context.DisplayOptions))</FoundIndexNote>
</ObsidianVaultDisplay>
}
</article>
</main>
</div>

@code {
#nullable enable

[Parameter]
public string PluginName { get; set; } = string.Empty;

[Parameter]
public string? PathSlug { get; set; }

private IVault? _vault;
private string _basePath = string.Empty;

private bool _loading;

Check warning on line 67 in src/YumeChan.NetRunner/Pages/Docs/DocsBrowser.razor

View workflow job for this annotation

GitHub Actions / build-debug

The field 'DocsBrowser._loading' is never used

Check warning on line 67 in src/YumeChan.NetRunner/Pages/Docs/DocsBrowser.razor

View workflow job for this annotation

GitHub Actions / build-release

The field 'DocsBrowser._loading' is never used

private Exception? _loadingError;
private ObsidianVaultDisplay _vaultDisplay = new();

protected override async Task OnParametersSetAsync()
{
_loadingError = null;
await base.OnParametersSetAsync();

// Edge case: if both parameters are null, don't load anything, and load the index page.
if (PluginName is null or "" && PathSlug is null or "")
{
return;
}

try
{
_vault = PluginDocsLoader.GetVault(PluginName) ?? throw new($"No vault found for plugin '{PluginName}'.");

Check warning on line 85 in src/YumeChan.NetRunner/Pages/Docs/DocsBrowser.razor

View workflow job for this annotation

GitHub Actions / build-debug

Possible null reference argument for parameter 'pluginName' in 'IVault? PluginDocsLoader.GetVault(string pluginName)'.

Check warning on line 85 in src/YumeChan.NetRunner/Pages/Docs/DocsBrowser.razor

View workflow job for this annotation

GitHub Actions / build-release

Possible null reference argument for parameter 'pluginName' in 'IVault? PluginDocsLoader.GetVault(string pluginName)'.
_basePath = $"/docs/{PluginName}/";
}
catch (DirectoryNotFoundException e)
{
_loadingError = e;
}
}

/// <summary>
/// Builds a Bootstrap breadcrumb using a slash-separated path string.
/// </summary>
/// <param name="path">The slash-separated path string.</param>
/// <returns>The RenderFragment for the breadcrumb.</returns>
RenderFragment BuildBreadcrumb(string? path) => __builder =>
{
path ??= string.Empty;

List<string> paths = new() { "." };
paths.AddRange(path.Split('/'));

<ul class="breadcrumb px-3">
@for (int i = 0; i < paths.Count; i++)
{
if (i is 0)
{
if (paths is { Count: 1 } or [.., ""])
{
// Render active home link
<li aria-current="page" class="breadcrumb-item active">~</li>
break;
}

// Render inactive home link
<li class="breadcrumb-item">
<a href="">~</a>
</li>

continue;
}

string pathName = paths[i];


if (i < paths.Count - 1)
{
string pathUrl = $"{string.Join("/", paths.Take(i + 1))}";

// Inactive breadcrumb item with link
<li class="breadcrumb-item">
<a href="@pathUrl">@pathName</a>
</li>
}
else
{
// Active breadcrumb item without link
<li aria-current="page" class="breadcrumb-item active">@pathName</li>
}
}
</ul>
};

}
13 changes: 13 additions & 0 deletions src/YumeChan.NetRunner/Pages/Docs/DocsBrowser.razor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.page {
position: relative;
display: flex;
flex-direction: column;

@media (min-width: 641px) {
flex-direction: row;
}
}

main {
flex-grow: 1;
}
1 change: 1 addition & 0 deletions src/YumeChan.NetRunner/Pages/_Host.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<link rel="stylesheet" href="/css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="/lib/bootstrap-icons/font/bootstrap-icons.min.css" />
<link rel="stylesheet" href="/css/site.css" />
<link rel="stylesheet" href="/YumeChan.NetRunner.styles.css" />
</head>
<body>
<component type="typeof(App)" render-mode="ServerPrerendered" />
Expand Down
6 changes: 3 additions & 3 deletions src/YumeChan.NetRunner/PluginRoute.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
@page "/plugin/{PluginName}/{*PathSlug}"
@page "/p/{PluginName}/{*PathSlug}"

<PluginRouter Plugin="_plugin" RoutePath=@(PathSlug ?? "/")>
<PluginRouter Plugin="@_plugin" RoutePath=@(PathSlug ?? "/")>
<Found Context="routeData">
<AuthorizeRouteView @ref=_authorizeRouteView RouteData=routeData />
<AuthorizeRouteView @ref="@_authorizeRouteView" RouteData=routeData />
</Found>

<NotFound>
<CascadingAuthenticationState>
@if (PathSlug is null)
{
@* This means we've hit the root of the plugin, yet no homepage for the plugin exists. *@
@* This means we've hit the root of the plugin, yet no homepage for the plugin exists. *@
<h5>Hmm... Seems like @PluginName has no plugin homepage.</h5>
}
else
Expand Down
2 changes: 1 addition & 1 deletion src/YumeChan.NetRunner/Shared/CoverLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


<header>
<Navbar CurrentUri=@CurrentUri />
<Navbar CurrentUri="@CurrentUri" />
</header>

<div class="container-fluid" style="margin: 100px 0px 0px inherit; padding: 0px;">
Expand Down
19 changes: 19 additions & 0 deletions src/YumeChan.NetRunner/Shared/Docs/DocsLayout.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@inherits CommonSiteLayout

<div class="container-fluid" style="margin-top: 4rem !important">
<div class="page">
<header>
<Navbar CurrentUri="@CurrentUri" />
</header>

<main role="main">
@Body
</main>
</div>
</div>

<Footer />

@code {

}
13 changes: 13 additions & 0 deletions src/YumeChan.NetRunner/Shared/Docs/DocsLayout.razor.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.page {
position: relative;
display: flex;
flex-direction: column;

@media (min-width: 641px) {
flex-direction: row;
}
}

main {
flex: 1;
}
Loading

0 comments on commit 3795d82

Please sign in to comment.