fsprojects / FSharp.Formatting

F# tools for generating documentation (Markdown processor and F# code formatter)
https://fsprojects.github.io/FSharp.Formatting/
Other
462 stars 155 forks source link

Customize the menu items of {{fsdocs-list-of-documents}} and {{fsdocs-list-of-namespaces}} #754

Closed nojaf closed 1 year ago

nojaf commented 1 year ago

Hello, we are currently experimenting with FsDoc. We aim to have our own template and are experimenting with Bootstrap 5, Sass and some other ideas.

We are currently hitting a limitation that:

{{fsdocs-list-of-documents}}
{{fsdocs-list-of-namespaces}}

is generating some HTML that doesn't fit our needs. Would there be a way to have some control over what this outputs?

//cc @yisusalanpng

dsyme commented 1 year ago

Certainly, you can suggest something?

nojaf commented 1 year ago

Well, I can think of two suggestions.

The first would be introducing another replacement variable that outputs JSON instead of HTML. {{fsdocs-list-of-documents-json}}. Having that opens the door to easily constructing HTML on the client.

The second would be to add an option to pass something that can transform https://github.com/nojaf/FSharp.Formatting/blob/82e0c8a09136fa56dcbcd931c290fcce97462970/src/fsdocs-tool/BuildCommand.fs#L553-L586. Maybe an F# script? Or provide an API endpoint, where some data will be posted and the response body is used as the replacement value. I'm not quite sure what could be elegant.

nojaf commented 1 year ago

@dsyme, I made an experiment with transforming the menu information in an outside endpoint. https://github.com/fsprojects/FSharp.Formatting/commit/a4d9f8f3fa3acbdb2337a57e35ebab4b48cccbac

I made a small script to act as the receiving endpoint:

#r "nuget: Suave"
#r "nuget: Thoth.Json.Net, 8.0.0"
#r "nuget: Fable.React, 8.0.1"

open System.Net
open Suave
open Suave.Filters
open Suave.Operators
open Suave.Successful
open Thoth.Json.Net

type Item = {
    Title: string
    Category: string
    CategoryIndex: int
    Index: int
    Link: string
}

let decodeStringAsInt = Decode.string |> Decode.map (int)

let decodeItem: Decoder<Item> =
    Decode.object (fun get -> {
        Category = get.Required.Field "category" Decode.string
        CategoryIndex = get.Required.Field "categoryIndex" decodeStringAsInt
        Index = get.Required.Field "index" decodeStringAsInt
        Link = get.Required.Field "link" Decode.string
        Title = get.Required.Field "title" Decode.string
    }
    )

open Fable.React
open Fable.React.Props

let view (items: Item array) : string =
    let groups = Array.groupBy (fun i -> i.CategoryIndex) items

    let children =
        groups
        |> Array.map (fun (_, groupItems) ->
            let groupTitle = groupItems[0].Category

            let id = $"menu-{groupTitle}-collapse".Replace(" ", "-").Trim().ToLower()

            let groupItems =
                groupItems
                |> Array.map (fun (gi: Item) ->
                    li [] [ a [ Href gi.Link; ClassName "ms-4 my-2 d-block" ] [ str gi.Title ] ]
                )

            li [ ClassName "mb-1" ] [
                button [
                    ClassName "btn align-items-center rounded"
                    Data("bs-toggle", "collapse")
                    Data("bs-target", $"#{id}")
                    AriaExpanded true
                ] [ str groupTitle ]
                div [ ClassName "collapse show"; Id id ] [
                    ul [ ClassName "list-unstyled fw-normal pb-1 small" ] groupItems
                ]
            ]
        )

    let element = fragment [] children
    Fable.ReactServer.renderToString (element)

let menuPart =
    POST
    >=> Filters.path "/menu"
    >=> (fun (ctx: HttpContext) -> async {
        let json = System.Text.Encoding.UTF8.GetString(ctx.request.rawForm)
        printfn "received: %s" json

        match Decode.fromString (Decode.array decodeItem) json with
        | Error err -> return! OK $"<div>Failed to decode, {err}</div>" ctx
        | Ok items ->
            let html = view items
            printfn "html:\n%s" html
            return! OK html ctx
    }
    )

let port = 8906us

startWebServer
    { defaultConfig with
        bindings = [ HttpBinding.create HTTP IPAddress.Loopback port ]
    }
    menuPart

This is quite the escape hatch, but we can do whatever we want inside our endpoint. And I think the impact for FsDocs is minimal. If anything fails, you are on your own, which should be the policy for such a malpractise.

dsyme commented 1 year ago

Adding json substitution seems dead simple. Please go ahead with that?

nojaf commented 1 year ago

After giving this some more thought and discussing it with a friend, I like to propose an alternative. What if we work with a _menu_template.html and _menu-item_template.html.

Where the default of _menu_template.html would be:

<li class="nav-header">
  {{fsdocs-menu-header-content}}
</li>
{{fsdocs-menu-items}}

and _menu-item_template.html

<li class="nav-item"><a href="{{fsdocs-menu-item-link}}" class="nav-link">{{fsdocs-menu-item-content}}</a></li>

If the additional template files are present, we would transform

https://github.com/fsprojects/FSharp.Formatting/blob/82e0c8a09136fa56dcbcd931c290fcce97462970/src/fsdocs-tool/BuildCommand.fs#L553-L586

accordingly. If not, just return the default behaviour.

dsyme commented 1 year ago

Seems reasonable!

nojaf commented 1 year ago

Thanks, we will prepare a concrete proposal in a PR!