" />

" />

" />
Class Sealed
public sealed class OpenApiPlugin : Moka.Docs.Plugins.IMokaPlugin

Namespace: Moka.Docs.Plugins.OpenApi

MokaDocs plugin that reads an OpenAPI 3.0 JSON specification file and generates beautiful endpoint documentation pages. Endpoints are grouped by tag, and each group receives its own detail page with parameter tables, request/response schemas, HTTP method badges, and example JSON payloads.

Configuration in mokadocs.yaml:


plugins:
  - name: openapi
    options:
      spec: ./openapi.json
      label: "REST API"
      routePrefix: /rest-api

Inheritance

Implements: Moka.Docs.Plugins.IMokaPlugin

Properties

NameDescription
Id Unique identifier for this plugin (e.g., "mokadocs-mermaid").
Name Human-readable display name.
Version Semantic version of this plugin.

Id

string OpenApiPlugin.Id { get; }

Unique identifier for this plugin (e.g., "mokadocs-mermaid").

Name

string OpenApiPlugin.Name { get; }

Human-readable display name.

Version

string OpenApiPlugin.Version { get; }

Semantic version of this plugin.

Methods

NameDescription
ExecuteAsync(…) Called during the build pipeline, giving the plugin a chance to modify the build context (add pages, transform content, etc.).
InitializeAsync(IPluginContext context, CancellationToken ct) Called once when the plugin is loaded. Use this to register services, subscribe to events, or validate configuration.

ExecuteAsync(IPluginContext context, BuildContext buildContext, CancellationToken ct)

Task OpenApiPlugin.ExecuteAsync(IPluginContext context, BuildContext buildContext, CancellationToken ct = null)

Called during the build pipeline, giving the plugin a chance to modify the build context (add pages, transform content, etc.).

Parameters

NameTypeDescription
contextMoka.Docs.Plugins.IPluginContextThe plugin context.
buildContextBuildContextThe current build context.
ctCancellationTokenCancellation token.

InitializeAsync(IPluginContext context, CancellationToken ct)

Task OpenApiPlugin.InitializeAsync(IPluginContext context, CancellationToken ct = null)

Called once when the plugin is loaded. Use this to register services, subscribe to events, or validate configuration.

Parameters

NameTypeDescription
contextMoka.Docs.Plugins.IPluginContextThe plugin context providing access to site services.
ctCancellationTokenCancellation token.
Type Relationships
classDiagram
                    style OpenApiPlugin fill:#f9f,stroke:#333,stroke-width:2px
                    OpenApiPlugin ..|> IMokaPlugin : implements
                
View Source
/// <summary>
///     MokaDocs plugin that reads an OpenAPI 3.0 JSON specification file and generates
///     beautiful endpoint documentation pages. Endpoints are grouped by tag, and each
///     group receives its own detail page with parameter tables, request/response schemas,
///     HTTP method badges, and example JSON payloads.
///     <para>
///         Configuration in <c>mokadocs.yaml</c>:
///         <code>
/// plugins:
///   - name: openapi
///     options:
///       spec: ./openapi.json
///       label: "REST API"
///       routePrefix: /rest-api
/// </code>
///     </para>
/// </summary>
public sealed class OpenApiPlugin : IMokaPlugin
{
    private string _label = "REST API";
    private string _routePrefix = "/api";
    private string _specPath = "";
    /// <inheritdoc/>
    public string Id => "openapi";
    /// <inheritdoc/>
    public string Name => "OpenAPI Documentation";
    /// <inheritdoc/>
    public string Version => "1.0.0";

    /// <inheritdoc/>
    public Task InitializeAsync(IPluginContext context, CancellationToken ct = default)
    {
        IReadOnlyDictionary<string, object> options = context.Options;
        if (options.TryGetValue("spec", out object? specObj) && specObj is string specStr)
        {
            _specPath = specStr;
        }

        if (options.TryGetValue("label", out object? labelObj) && labelObj is string labelStr)
        {
            _label = labelStr;
        }

        if (options.TryGetValue("routePrefix", out object? prefixObj) && prefixObj is string prefixStr)
        {
            _routePrefix = prefixStr.TrimEnd('/');
        }

        if (string.IsNullOrWhiteSpace(_specPath))
        {
            context.LogWarning("OpenAPI plugin: 'spec' option is not set. " + "Will attempt auto-discovery of openapi.json or swagger.json.");
        }

        context.LogInfo($"OpenAPI plugin initialized (spec={_specPath}, label={_label}, routePrefix={_routePrefix})");
        return Task.CompletedTask;
    }

    /// <inheritdoc/>
    public async Task ExecuteAsync(IPluginContext context, BuildContext buildContext, CancellationToken ct = default)
    {
        string? specFullPath = ResolveSpecPath(buildContext);
        if (specFullPath is null)
        {
            context.LogWarning("OpenAPI plugin: No OpenAPI spec file found. Skipping.");
            return;
        }

        context.LogInfo($"OpenAPI plugin: Processing spec at {specFullPath}");
        string json;
        try
        {
            json = await File.ReadAllTextAsync(specFullPath, ct);
        }
        catch (Exception ex)
        {
            context.LogError($"OpenAPI plugin: Failed to read spec file '{specFullPath}': {ex.Message}");
            return;
        }

        OpenApiSpec spec;
        try
        {
            spec = OpenApiParser.Parse(json);
        }
        catch (Exception ex)
        {
            context.LogError($"OpenAPI plugin: Failed to parse spec file '{specFullPath}': {ex.Message}");
            return;
        }

        List<DocPage> pages = GeneratePages(spec, context);
        buildContext.Pages.AddRange(pages);
        context.LogInfo($"OpenAPI plugin: Generated {pages.Count} pages ({spec.Endpoints.Count} endpoints) from '{specFullPath}'");
    }

    /// <summary>
    ///     Resolves the OpenAPI spec file path. If the user specified a path in the config,
    ///     resolves it relative to the project root. Otherwise, tries common file names.
    /// </summary>
    private string? ResolveSpecPath(BuildContext buildContext)
    {
        string root = buildContext.RootDirectory;
        // User-specified path
        if (!string.IsNullOrWhiteSpace(_specPath))
        {
            string full = Path.IsPathRooted(_specPath) ? _specPath : Path.GetFullPath(Path.Combine(root, _specPath));
            return File.Exists(full) ? full : null;
        }

        // Auto-discovery (JSON and YAML)
        string[] candidates = ["openapi.json", "openapi.yaml", "openapi.yml", "swagger.json", "swagger.yaml"];
        string[] directories = [root, Path.Combine(root, buildContext.Config.Content.Docs)];
        foreach (string dir in directories)
        {
            if (!Directory.Exists(dir))
            {
                continue;
            }

            foreach (string candidate in candidates)
            {
                string path = Path.Combine(dir, candidate);
                if (File.Exists(path))
                {
                    return path;
                }
            }
        }

        return null;
    }

    /// <summary>
    ///     Generates all documentation pages from the parsed spec: one index page
    ///     and one detail page per tag group.
    /// </summary>
    private List<DocPage> GeneratePages(OpenApiSpec spec, IPluginContext context)
    {
        var pages = new List<DocPage>();
        string inlineCss = OpenApiPageRenderer.GetInlineCss();
        // Group endpoints by tag
        var grouped = new Dictionary<string, List<OpenApiEndpoint>>(StringComparer.OrdinalIgnoreCase);
        foreach (OpenApiEndpoint ep in spec.Endpoints)
        {
            List<string> tags = ep.Tags.Count > 0 ? ep.Tags : ["Other"];
            foreach (string tag in tags)
            {
                if (!grouped.TryGetValue(tag, out List<OpenApiEndpoint>? list))
                {
                    list = [];
                    grouped[tag] = list;
                }

                list.Add(ep);
            }
        }

        // Index page
        string indexHtml = inlineCss + OpenApiPageRenderer.RenderIndexPage(spec, _routePrefix);
        pages.Add(new DocPage { FrontMatter = new FrontMatter { Title = _label, Description = spec.Description, Icon = "code" }, Content = new PageContent { Html = indexHtml, PlainText = $"{_label} - {spec.Title} - {spec.Description}" }, Route = _routePrefix, Origin = PageOrigin.ApiGenerated });
        // Per-tag detail pages
        int order = 1;
        foreach ((string tag, List<OpenApiEndpoint> endpoints)in grouped.OrderBy(kv => kv.Key))
        {
            string slug = Slugify(tag);
            string tagHtml = inlineCss + OpenApiPageRenderer.RenderTagPage(tag, endpoints);
            var plainTextParts = new List<string>
            {
                tag
            };
            foreach (OpenApiEndpoint ep in endpoints)
            {
                plainTextParts.Add($"{ep.Method} {ep.Path} {ep.Summary}");
            }

            pages.Add(new DocPage { FrontMatter = new FrontMatter { Title = tag, Description = $"{_label} - {tag}", Order = order++ }, Content = new PageContent { Html = tagHtml, PlainText = string.Join(" ", plainTextParts) }, Route = $"{_routePrefix}/{slug}", Origin = PageOrigin.ApiGenerated });
            context.LogInfo($"OpenAPI plugin: Generated page for tag '{tag}' with {endpoints.Count} endpoints");
        }

        return pages;
    }

    private static string Slugify(string value)
    {
        return value.ToLowerInvariant().Replace(' ', '-').Replace('/', '-').Replace('.', '-');
    }
}
Was this page helpful?