packages feed

hyperbole-0.7.0: docs/Docgen.hs

module Main where

import Data.Char (isAlpha, isSpace)
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import System.Directory
import System.Environment (getArgs)
import System.FilePath


main :: IO ()
main = do
  args <- getArgs
  case args of
    [original, input, output] -> do
      src <- readSource input
      expanded <- expandFile src
      let linePragma = T.pack $ "{-# LINE 1 \"" ++ original ++ "\" #-}"
      let final = SourceCode [linePragma] <> expanded
      T.writeFile output $ T.unlines final.lines
    _ -> error "Usage (Hyperbole Internal Only): docgen src/MyModule.hs /tmp/input/MyModule.hs /build/output/Original.hs"


-- test :: IO ()
-- test = do
--   src <- readSource "./src/Web/Hyperbole.hs"
--   SourceCode lns <- expandFile src
--   mapM_ print lns

-- expandSourcesTo :: FilePath -> IO ()
-- expandSourcesTo tmpDir = do
--   allFiles <- relativeSourceFiles "./src"
--   -- mapM_ (putStrLn . ("SOURCE " <>)) allFiles
--   mapM_ (expandAndCopyFileTo tmpDir) allFiles

-- copyExtraFilesTo :: FilePath -> IO ()
-- copyExtraFilesTo tmpDir = do
--   createDirectoryIfMissing True tmpDir
--   copyFile "./cabal.project" (tmpDir </> "cabal.project")
--   copyFile "./hyperbole.cabal" (tmpDir </> "hyperbole.cabal")
--   copyFile "./README.md" (tmpDir </> "README.md")
--   copyFile "./CHANGELOG.md" (tmpDir </> "CHANGELOG.md")
--   copyFile "./LICENSE" (tmpDir </> "LICENSE")
--   createDirectoryIfMissing True (tmpDir </> "client/dist")
--   copyFile "./client/dist/hyperbole.js" (tmpDir </> "client/dist/hyperbole.js")
--   copyFile "./client/dist/hyperbole.js.map" (tmpDir </> "client/dist/hyperbole.js.map")
--   createDirectoryIfMissing True (tmpDir </> "client/util")
--   copyFile "./client/util/live-reload.js" (tmpDir </> "client/util/live-reload.js")

-- expandAndCopyFileTo :: FilePath -> FilePath -> IO ()
-- expandAndCopyFileTo tmpDir pth = do
--   putStrLn $ "EXPANDING " <> pth
--   src <- readSource pth
--   expanded <- expandFile src
--   writeSource tmpDir pth expanded

readSource :: FilePath -> IO SourceCode
readSource pth = do
  inp <- T.readFile pth
  pure $ SourceCode $ T.lines inp


writeSource :: FilePath -> FilePath -> SourceCode -> IO ()
writeSource tmpDir relPath src = do
  let pth = tmpDir </> cleanRelativeDir relPath
  -- putStrLn $ "WRITE " <> pth <> " " <> show (length src.lines)
  createDirectoryIfMissing True $ takeDirectory pth
  T.writeFile pth $ T.unlines src.lines
 where
  cleanRelativeDir =
    dropWhile (== '/') . dropWhile (== '.')


-- relativeSourceFiles :: FilePath -> IO [FilePath]
-- relativeSourceFiles dir = do
--   contents <- tryDirectory dir
--   let folders = filter isFolder contents
--   let files = filter isSourceFile contents
--
--   files' <- mapM (relativeSourceFiles . addDir) folders
--
--   pure $ fmap addDir files <> mconcat files'
--  where
--   isSourceFile pth = takeExtension pth == ".hs"
--   isFolder pth = takeExtension pth == ""
--   addDir = (dir </>)
--   tryDirectory pth = do
--     res <- try $ listDirectory pth
--     case res of
--       Left (_ :: SomeException) -> do
--         putStrLn $ "SKIPPED" <> pth
--         pure []
--       Right files -> pure files

data Macro
  = Embed
  { moduleName :: ModuleName
  , definition :: TopLevelDefinition
  }
  deriving (Eq)
newtype SourceCode = SourceCode {lines :: [Text]}
  deriving newtype (Monoid, Semigroup)
instance Show Macro where
  -- show (Example p) = "Example " <> show p
  show (Embed mn def) = "Embed " <> show mn <> " " <> show def


newtype ModuleName = ModuleName Text
  deriving newtype (Eq, Show)


newtype TopLevelDefinition = TopLevelDefinition Text
  deriving newtype (Show, Eq)


expandFile :: SourceCode -> IO SourceCode
expandFile (SourceCode lns) =
  SourceCode . mconcat <$> mapM expandLine lns


-- > EMBED Example/Docs/BasicPage.hs page
expandLine :: Text -> IO [Text]
expandLine line = do
  case parseMacro line of
    Nothing -> do
      pure [line]
    Just (pre, Embed src def) -> do
      expandEmbed src pre def
 where
  -- Just (pre, Example src) -> do
  --   expandExample src pre

  parseMacro :: Text -> Maybe (Text, Macro)
  parseMacro inp = do
    parseEmbed inp -- <|> parseExample inp

  -- parseExample l = do
  --   case T.splitOn "#EXAMPLE " l of
  --     [prefix, src] -> do
  --       pure (prefix, Example $ path src)
  --     _ -> Nothing

  parseEmbed l = do
    case T.splitOn "#EMBED " l of
      [prefix, info] -> do
        (mn, definition) <- splitSrcDef $ T.dropWhile (== ' ') info
        pure (prefix, Embed mn definition)
      _ -> Nothing

  splitSrcDef inp =
    let (mn, def) = T.breakOn " " inp
     in pure (ModuleName mn, TopLevelDefinition $ T.drop 1 def)


-- look it up as a URI...

-- * #EXAMPLE /simple


-- expandExample :: Path -> Text -> IO [Text]
-- expandExample p prefix = do
--   let pre = if T.null prefix then "▶️ " else prefix
--   r <- appRoute
--   pure [pre <> "[" <> routeTitle r <> "](" <> uriToText (exampleBaseURI ./. p) <> ")"]
--  where
--   appRoute :: IO AppRoute
--   appRoute = do
--     case matchRoute @AppRoute p of
--       Nothing -> fail $ "Could not find example: " <> cs (pathToText False p)
--       Just r -> pure r

-- exampleBaseURI :: URI
-- exampleBaseURI = [uri|https://hyperbole.live|]

modulePath :: ModuleName -> FilePath
modulePath (ModuleName mn) = cs $ T.replace "." "/" mn <> ".hs"


expandEmbed :: ModuleName -> Text -> TopLevelDefinition -> IO [Text]
expandEmbed mn pfx def = do
  let src = modulePath mn
  -- putStrLn $ "  embed: " <> src
  source <- T.readFile $ "./demo/" <> src
  expanded <- requireTopLevel def (SourceCode $ T.lines source)
  pure $ fmap markupLine expanded
 where
  requireTopLevel :: TopLevelDefinition -> SourceCode -> IO [Text]
  requireTopLevel tld sc =
    case findTopLevel tld sc of
      [] -> fail $ "Could not find: " <> show (Embed mn def) <> " " <> show def
      lns -> pure lns

  -- addPrefix line = embed.prefix <> line
  markupLine :: Text -> Text
  markupLine line =
    case pfx of
      "" -> markupLineAt line
      _ -> markupLinePrefix line
  markupLineAt =
    T.replace "\"" "\\\"" . highlightTermsLine
  markupLinePrefix line =
    pfx <> line


highlightTermsLine :: Text -> Text
highlightTermsLine ln = mconcat $ fmap highlightWord $ T.groupBy isSameTerm ln
 where
  isSameTerm :: Char -> Char -> Bool
  isSameTerm c1 c2 =
    (isAlpha c1 && isAlpha c2)
      || (isSpace c1 && isSpace c2)

  highlightWord :: Text -> Text
  highlightWord w =
    if w `elem` terms
      then "'" <> w <> "'"
      else w

  terms :: [Text]
  terms =
    [ "HyperView"
    , "View"
    , "Action"
    , "update"
    , "hyper"
    , "Page"
    , "liveApp"
    , "quickStartDocument"
    , "runPage"
    , "run"
    , "ViewId"
    , "viewId"
    , "ViewAction"
    , "Eff"
    , "button"
    , "el"
    , "el_"
    , "Hyperbole"
    , "Route"
    , "routeRequest"
    , "route"
    , "layout"
    , "Response"
    , "ToParam"
    , "FromParam"
    , "Session"
    , "FromQuery"
    , "ToQuery"
    , "lookupParam"
    , "setParam"
    , "DefaultParam"
    , "Client"
    ]


-- returns lines of a top-level definition
findTopLevel :: TopLevelDefinition -> SourceCode -> [Text]
findTopLevel (TopLevelDefinition definition) source =
  let rest = dropWhile (not . isTopLevel) source.lines
   in dropWhileEnd isEmpty $ takeWhile isCurrentDefinition rest
 where
  isTopLevel = T.isPrefixOf definition
  isEmpty = T.null
  -- isBlankLine line = T.null $ T.strip line
  isCurrentDefinition line =
    isTopLevel line || not (isFullyOutdented line)
  dropWhileEnd p as =
    reverse $ dropWhile p $ reverse as


isFullyOutdented :: Text -> Bool
isFullyOutdented line =
  case cs (T.take 1 line) of
    "" -> False
    [c] -> not $ isSpace c
    _ -> False