packages feed

mdoc-0.5.0.0: src/Mdoc/Data/Synopsis.hs

-- |
--
-- Module      : Mdoc.Data.Synopsis
-- Copyright   : (c) 2026 Patrick Brisbin
-- License     : AGPL-3
-- Maintainer  : pbrisbin@gmail.com
-- Stability   : experimental
-- Portability : POSIX
module Mdoc.Data.Synopsis
  ( Synopsis
  , singleton
  , alternation
  , command
  ) where

import Mdoc.Prelude

import Data.Char (toLower)
import Data.List (sort, sortOn)
import Mdoc.Data.Described
import Mdoc.Data.Flag
import Mdoc.Data.Optionality
import Mdoc.Data.Positional
import Mdoc.Optics
import Mdoc.Pretty
import Mdoc.Syntax

newtype Synopsis = Synopsis
  { items :: [SynopsisItem]
  }
  deriving stock (Eq, Show)
  deriving newtype (Monoid, Semigroup)
  deriving (ToJSON) via (Rendered Synopsis)

instance Pretty Synopsis where
  pretty = renderUsage . (.items)

data SynopsisItem
  = Single (Described Positional)
  | Alternation Optionality [Described Positional]
  | Command
  deriving stock (Eq, Show)

instance Ord SynopsisItem where
  compare = comparing getFirstPositional
   where
    getFirstPositional :: SynopsisItem -> Maybe (Described Positional)
    getFirstPositional = \case
      Single p -> Just p
      Alternation _ (p : _) -> Just p
      _ -> Nothing

singleton :: Described Positional -> Synopsis
singleton = Synopsis . pure . Single

alternation :: Optionality -> [Described Positional] -> Synopsis
alternation o = Synopsis . pure . Alternation o

command :: Synopsis
command = Synopsis [Command]

data Usage ann = Usage
  { chars :: [Char]
  -- ^ list those [short] options not taking arguments first, in alphabetical
  -- order
  , shorts :: [Doc ann]
  -- ^ After that, list all [short] options taking arguments
  , longs :: [Doc ann]
  -- ^ If there are long options, list them after the short options that take
  -- arguments, just before the command line arguments
  , args :: [Doc ann]
  -- ^ Arguments follow the last option
  , command :: [Doc ann]
  }
  deriving stock (Generic, Show)
  deriving (Monoid, Semigroup) via Generically (Usage ann)

usageSetter :: Positional -> Lens' (Usage ann) [Doc ann]
usageSetter = \case
  PositionalOption (Option {flag = Flag {}}) -> field @"shorts"
  PositionalOption (Option {flag = GNUFlag {}}) -> field @"longs"
  PositionalSwitch (GNUFlag {}) -> field @"longs"
  PositionalSwitch (Flag {}) -> field @"shorts" -- edge-case
  PositionalArgument {} -> field @"args"

renderUsage :: [SynopsisItem] -> Doc ann
renderUsage = go . buildUsage
 where
  go u =
    let ws =
          nonEmpty
            $ maybeToList (pChars <$> nonEmpty u.chars)
              <> u.shorts
              <> u.longs
              <> u.args
              <> u.command
    in  case ws of
          Nothing -> mempty
          Just ne -> vsep $ ".Bk -words" : toList ne <> [".Ek"]

  pChars cs = ".Op Fl" <+> pretty (fixMacro $ esc $ pack $ toList cs)

-- | Fix for when short-switches accidentally become a macro name
--
-- For example, @docker(1)@ has short switches @-D@ and @-v@, which become:
--
-- @
-- .Op Fl Dv
-- @
--
-- We have to escape the @Dv@ since it's a macro name.
--
-- It's safe to do this always, I guess, but also easy to only do it if needed.
fixMacro :: Text -> Text
fixMacro t
  | t `elem` macroNames = "\\&" <> t
  | otherwise = t

buildUsage :: [SynopsisItem] -> Usage ann
buildUsage = foldl addUsage mempty . sort

addUsage :: Usage ann -> SynopsisItem -> Usage ann
addUsage acc = \case
  Single (Described {item = PositionalSwitch (Flag c _)}) ->
    acc & field @"chars" %~ sortOn toLower . (<> [c])
  Single d ->
    acc
      & usageSetter d.item
        <>~ [ opLine d
                $ ellipsis d
                $ pretty
                $ Positional1 d.item
            ]
  Alternation _ [] -> acc
  Alternation o ds@(d : _) ->
    acc
      & usageSetter d.item
        <>~ [ opLine' o
                $ mconcat
                $ punctuate " | "
                $ map (pretty . Positional1 . (.item)) ds
            ]
  Command -> acc & field @"command" <>~ [".Ar command"]

opLine :: Described a -> Doc ann -> Doc ann
opLine = opLine' . (.optionality)

opLine' :: Optionality -> Doc ann -> Doc ann
opLine' = \case
  Required -> ("." <>)
  _ -> (".Op" <+>)

ellipsis :: Described a -> Doc ann -> Doc ann
ellipsis d
  | d.multiple = (<+> "...")
  | otherwise = id