Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
nojaf opened this issue Jul 14, 2022 · 7 comments

Comments

@nojaf
Copy link
Collaborator

nojaf commented Jul 14, 2022

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
Copy link
Contributor

dsyme commented Jul 16, 2022

Certainly, you can suggest something?

@nojaf
Copy link
Collaborator Author

nojaf commented Jul 18, 2022

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
Copy link
Collaborator Author

nojaf commented Jul 19, 2022

@dsyme, I made an experiment with transforming the menu information in an outside endpoint.
a4d9f8f

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
Copy link
Contributor

dsyme commented Jul 22, 2022

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

@nojaf
Copy link
Collaborator Author

nojaf commented Jul 26, 2022

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

[
// No categories specified
if modelsByCategory.Length = 1 && (fst modelsByCategory.[0]) = None then
li [ Class "nav-header" ] [ !! "Documentation" ]
for model in snd modelsByCategory.[0] do
let link = model.Uri(root)
li [ Class "nav-item" ] [ a [ Class "nav-link"; (Href link) ] [ encode model.Title ] ]
else
// At least one category has been specified. Sort each category by index and emit
// Use 'Other' as a header for uncategorised things
for (cat, modelsInCategory) in modelsByCategory do
let modelsInCategory =
modelsInCategory
|> List.sortBy (fun model ->
match model.Index with
| Some s ->
(try
int32 s
with _ ->
Int32.MaxValue)
| None -> Int32.MaxValue)
match cat with
| Some c -> li [ Class "nav-header" ] [ !!c ]
| None -> li [ Class "nav-header" ] [ !! "Other" ]
for model in modelsInCategory do
let link = model.Uri(root)
li [ Class "nav-item" ] [ a [ Class "nav-link"; (Href link) ] [ encode model.Title ] ] ]
|> List.map (fun html -> html.ToString())
|> String.concat " \n"

accordingly. If not, just return the default behaviour.

@dsyme
Copy link
Contributor

dsyme commented Jul 29, 2022

Seems reasonable!

@nojaf
Copy link
Collaborator Author

nojaf commented Jul 29, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants