From a85df04fcb2876591ac39d55e920c7cd15411431 Mon Sep 17 00:00:00 2001
From: Valentin Gagarin <valentin.gagarin@tweag.io>
Date: Fri, 26 Aug 2022 23:09:19 +0200
Subject: [PATCH] refactor showOptions

it was quite a pain to manipulate strings just with `builtins`
---
 doc/manual/generate-manpage.nix | 45 ++++++++++++++-------------------
 doc/manual/utils.nix            | 26 +++++++++++++++++++
 2 files changed, 45 insertions(+), 26 deletions(-)

diff --git a/doc/manual/generate-manpage.nix b/doc/manual/generate-manpage.nix
index c8290685a..82aae8b46 100644
--- a/doc/manual/generate-manpage.nix
+++ b/doc/manual/generate-manpage.nix
@@ -41,33 +41,26 @@ let
 
         ${showOptions def.flags}
       '';
-      showOptions = flags:
+      showOptions = options:
         let
-          categories = sort builtins.lessThan (unique (map (cmd: cmd.category) (attrValues flags)));
-        in
-          concatStrings (map
-            (cat:
-              (if cat != ""
-               then "**${cat}:**\n\n"
-               else "")
-              + concatStrings
-                (map (longName:
-                  let
-                    flag = flags.${longName};
-                  in
-                    "  - `--${longName}`"
-                    + (if flag ? shortName then " / `-${flag.shortName}`" else "")
-                    + (if flag ? labels then " " + (concatStringsSep " " (map (s: "*${s}*") flag.labels)) else "")
-                    + "  \n"
-                    + "    " + flag.description + "\n\n"
-                ) (attrNames (filterAttrs (n: v: v.category == cat) flags))))
-            categories);
-      squash = string: # squash more than two repeated newlines
-        let
-          replaced = replaceStrings [ "\n\n\n" ] [ "\n\n" ] string;
-        in
-          if replaced == string then string else squash replaced;
-    in squash ''
+          showCategory = cat: ''
+            ${if cat != "" then "**${cat}:**" else ""}
+
+            ${listOptions (filterAttrs (n: v: v.category == cat) options)}
+            '';
+          listOptions = opts: concatStringsSep "\n" (attrValues (mapAttrs showOption opts));
+          showOption = name: option:
+            let
+              shortName = if option ? shortName then "/ `-${option.shortName}`" else "";
+              labels = if option ? labels then (concatStringsSep " " (map (s: "*${s}*") option.labels)) else "";
+            in trim ''
+              - `--${name}` ${shortName} ${labels}
+
+                ${option.description}
+            '';
+          categories = sort builtins.lessThan (unique (map (cmd: cmd.category) (attrValues options)));
+        in concatStrings (map showCategory categories);
+    in squash  ''
       > **Warning** \
       > This program is **experimental** and its interface is subject to change.
 
diff --git a/doc/manual/utils.nix b/doc/manual/utils.nix
index d4b18472f..d0643ef46 100644
--- a/doc/manual/utils.nix
+++ b/doc/manual/utils.nix
@@ -5,6 +5,32 @@ rec {
 
   concatStrings = concatStringsSep "";
 
+  replaceStringsRec = from: to: string:
+    # recursively replace occurrences of `from` with `to` within `string`
+    # example:
+    #     replaceStringRec "--" "-" "hello-----world"
+    #     => "hello-world"
+    let
+      replaced = replaceStrings [ from ] [ to ] string;
+    in
+      if replaced == string then string else replaceStringsRec from to replaced;
+
+  squash = replaceStringsRec "\n\n\n" "\n\n";
+
+  trim = string:
+    # trim trailing spaces and squash non-leading spaces
+    let
+      trimLine = line:
+        let
+          # separate leading spaces from the rest
+          parts = split "(^ *)" line;
+          spaces = head (elemAt parts 1);
+          rest = elemAt parts 2;
+          # drop trailing spaces
+          body = head (split " *$" rest);
+        in spaces + replaceStringsRec "  " " " body;
+    in concatStringsSep "\n" (map trimLine (splitLines string));
+
   # FIXME: O(n^2)
   unique = foldl' (acc: e: if elem e acc then acc else acc ++ [ e ]) [];