DirTreeHtml (Polyglot)¶

In [ ]:
#r @"../../../../../../../.nuget/packages/fsharp.control.asyncseq/3.2.1/lib/netstandard2.1/FSharp.Control.AsyncSeq.dll"
#r @"../../../../../../../.nuget/packages/system.reactive/6.0.1-preview.1/lib/net6.0/System.Reactive.dll"
#r @"../../../../../../../.nuget/packages/system.reactive.linq/6.0.1-preview.1/lib/netstandard2.0/System.Reactive.Linq.dll"
#r @"../../../../../../../.nuget/packages/argu/6.2.4/lib/netstandard2.0/Argu.dll"
#r @"../../../../../../../.nuget/packages/falco.markup/1.1.1/lib/netstandard2.0/Falco.Markup.dll"
In [ ]:
#!import ../../lib/fsharp/Notebooks.dib
#!import ../../lib/fsharp/Testing.dib
In [ ]:
#!import ../../lib/fsharp/Common.fs
#!import ../../lib/fsharp/CommonFSharp.fs
#!import ../../lib/fsharp/Async.fs
#!import ../../lib/fsharp/AsyncSeq.fs
#!import ../../lib/fsharp/Runtime.fs
#!import ../../lib/fsharp/FileSystem.fs
In [ ]:
#if !INTERACTIVE
open Lib
#endif
In [ ]:
open SpiralFileSystem.Operators
open Falco.Markup
In [ ]:
type FileSystemNode =
    | File of string * string * int64
    | Folder of string * string * FileSystemNode list
    | Root of FileSystemNode list

let rec scanDirectory isRoot (basePath : string) (path : string) =
    let relativePath =
        path
        |> SpiralSm.replace basePath ""
        |> SpiralSm.replace "\\" "/"
        |> SpiralSm.replace "//" "/"
        |> SpiralSm.trim_start [| '/' |]

    let directories =
        path
        |> System.IO.Directory.GetDirectories
        |> Array.toList
        |> List.sort
        |> List.map (scanDirectory false basePath)
    let files =
        path
        |> System.IO.Directory.GetFiles
        |> Array.toList
        |> List.sort
        |> List.map (fun f -> File (System.IO.Path.GetFileName f, relativePath, System.IO.FileInfo(f).Length))

    let children = directories @ files
    if isRoot
    then Root children
    else Folder (path |> System.IO.Path.GetFileName, relativePath, children)

let rec generateHtml fsNode =
    let sizeLabel size =
        match float size with
        | size when size > 1024.0 * 1024.0 -> $"%.2f{size / 1024.0 / 1024.0} MB"
        | size when size > 1024.0 -> $"%.2f{size / 1024.0} KB"
        | size -> $"%.2f{size} B"
    match fsNode with
    | File (fileName, relativePath, size) ->
        Elem.div [] [
            Text.raw "📄 "
            Elem.a [
                Attr.href $"""{relativePath}{if relativePath = "" then "" else "/"}{fileName}"""
            ] [
                Text.raw fileName
            ]
            Elem.span [] [
                Text.raw $" ({size |> sizeLabel})"
            ]
        ]
    | Folder (folderName, relativePath, children) ->
        let size =
            let rec 루프 children =
                children
                |> List.sumBy (function
                    | File (_, _, size) -> size
                    | Folder (_, _, children)
                    | Root children -> 루프 children
                )
            루프 children
        Elem.details [
            Attr.open' "true"
        ] [
            Elem.summary [] [
                Text.raw "📂 "
                Elem.a [
                    Attr.href relativePath
                ] [
                    Text.raw folderName
                ]
                Elem.span [] [
                    Text.raw $" ({size |> sizeLabel})"
                ]
            ]
            Elem.div [] [
                yield! children |> List.map generateHtml
            ]
        ]
    | Root children ->
        Elem.div [] [
            yield! children |> List.map generateHtml
        ]

let generateHtmlForFileSystem root =
    $"""<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <style>
body {{
    background-color: #222;
    color: #ccc;
}}
a {{
  color: #777;
  font-size: 15px;
}}
span {{
  font-size: 11px;
}}
div > div {{
  padding-left: 10px;
}}
details > div {{
  padding-left: 19px;
}}
  </style>
</head>
<body>
  {root |> generateHtml |> renderNode}
</body>
</html>
"""
In [ ]:
//// test

let expected = """<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <style>
body {
    background-color: #222;
    color: #ccc;
}
a {
  color: #777;
  font-size: 15px;
}
span {
  font-size: 11px;
}
div > div {
  padding-left: 10px;
}
details > div {
  padding-left: 19px;
}
  </style>
</head>
<body>
  <div><details open="true"><summary>&#128194; <a href="_.root">_.root</a><span> (10.00 B)</span></summary><div><details open="true"><summary>&#128194; <a href="_.root/3">3</a><span> (6.00 B)</span></summary><div><details open="true"><summary>&#128194; <a href="_.root/3/2">2</a><span> (3.00 B)</span></summary><div><details open="true"><summary>&#128194; <a href="_.root/3/2/1">1</a><span> (1.00 B)</span></summary><div><div>&#128196; <a href="_.root/3/2/1/file.txt">file.txt</a><span> (1.00 B)</span></div></div></details><div>&#128196; <a href="_.root/3/2/file.txt">file.txt</a><span> (2.00 B)</span></div></div></details><div>&#128196; <a href="_.root/3/file.txt">file.txt</a><span> (3.00 B)</span></div></div></details><div>&#128196; <a href="_.root/file.txt">file.txt</a><span> (4.00 B)</span></div></div></details></div>
</body>
</html>
"""

let struct (tempFolder, disposable) = expected |> SpiralCrypto.hash_text |> SpiralFileSystem.create_temp_dir'
let rec 루프 d n = async {
    if n >= 0 then
        tempFolder </> d |> System.IO.Directory.CreateDirectory |> ignore
        do!
            n
            |> string
            |> String.replicate (n + 1)
            |> SpiralFileSystem.write_all_text_async (tempFolder </> d </> $"file.txt")
        do! 루프 $"{d}/{n}" (n - 1)
}
루프 "_.root" 3
|> Async.RunSynchronously

let html =
    scanDirectory true tempFolder tempFolder
    |> generateHtmlForFileSystem

html
|> _assertEqual expected

disposable.Dispose ()

html |> Microsoft.DotNet.Interactive.Formatting.Html.ToHtmlContent
📂 _.root (10.00 B)
📂 3 (6.00 B)
📂 2 (3.00 B)
📂 1 (1.00 B)
📄 file.txt (1.00 B)
📄 file.txt (2.00 B)
📄 file.txt (3.00 B)
📄 file.txt (4.00 B)
"<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <style>
body {
    background-color: #222;
    color: #ccc;
}
a {
  color: #777;
  font-size: 15px;
}
span {
  font-size: 11px;
}
div > div {
  padding-left: 10px;
}
details > div {
  padding-left: 19px;
}
  </style>
</head>
<body>
  <div><details open="true"><summary>&#128194; <a href="_.root">_.root</a><span> (10.00 B)</span></summary><div><details open="true"><summary>&#128194; <a href="_.root/3">3</a><span> (6.00 B)</span></summary><div><details open="true"><summary>&#128194; <a href="_.root/3/2">2</a><span> (3.00 B)</span></summary><div><details open="true"><summary>&#128194; <a href="_.root/3/2/1">1</a><span> (1.00 B)</span></summary><div><div>&#128196; <a href="_.root/3/2/1/file.txt">file.txt</a><span> (1.00 B)</span></div></div></details><div>&#128196; <a href="_.root/3/2/file.txt">file.txt</a><span> (2.00 B)</span></div></div></details><div>&#128196; <a href="_.root/3/file.txt">file.txt</a><span> (3.00 B)</span></div></div></details><div>&#128196; <a href="_.root/file.txt">file.txt</a><span> (4.00 B)</span></div></div></details></div>
</body>
</html>
"

Arguments¶

In [ ]:
[<RequireQualifiedAccess>]
type Arguments =
    | [<Argu.ArguAttributes.ExactlyOnce>] Dir of string
    | [<Argu.ArguAttributes.ExactlyOnce>] Html of string

    interface Argu.IArgParserTemplate with
        member s.Usage =
            match s with
            | Dir _ -> nameof Dir
            | Html _ -> nameof Html
In [ ]:
//// test

Argu.ArgumentParser.Create<Arguments>().PrintUsage ()
"USAGE: dotnet-repl [--help] --dir <string> --html <string>

OPTIONS:

    --dir <string>        Dir
    --html <string>       Html
    --help                display this list of options.
"

main¶

In [ ]:
let main args =
    let argsMap = args |> Runtime.parseArgsMap<Arguments>

    let dir =
        match argsMap.[nameof Arguments.Dir] with
        | [ Arguments.Dir dir ] -> Some dir
        | _ -> None
        |> Option.get

    let htmlPath =
        match argsMap.[nameof Arguments.Html] with
        | [ Arguments.Html html ] -> Some html
        | _ -> None
        |> Option.get

    let fileSystem = scanDirectory true dir dir
    let html = generateHtmlForFileSystem fileSystem

    html |> SpiralFileSystem.write_all_text_async htmlPath
    |> Async.runWithTimeout 30000
    |> function
        | Some () -> 0
        | None -> 1
In [ ]:
//// test

let args =
    System.Environment.GetEnvironmentVariable "ARGS"
    |> SpiralRuntime.split_args
    |> Result.toArray
    |> Array.collect id

match args with
| [||] -> 0
| args -> if main args = 0 then 0 else failwith "main failed"
0