packages feed

the-snip-0.1.0.0: src/Run.hs

module Run (run, snipStartT, snipEndT) where

import Import
import Path.Posix
import Path.IO
import Data.Attoparsec.Text as Parser
import RIO.Text qualified as T
import RIO.List qualified as L

data LeadingWS = NoLeadingWS | LeadingSpaces Word | LeadingTabs Word deriving (Show)

data ParsedLine = EmptyLine | StartMarkerLine (Maybe (Path Rel File)) | EndMarkerLine (Maybe (Path Rel File)) | ContentLine (LeadingWS,Text) deriving (Show)

mkContentLine :: Text -> ParsedLine
mkContentLine content = ContentLine $ case T.uncons content of
  Nothing -> (NoLeadingWS, "")
  Just ('\t', _) -> 
    let (tabs,rest) = T.span ('\t' ==) content in
    (LeadingTabs (fromIntegral $ T.length tabs), T.copy rest)
  Just (' ', _) -> 
    let (spaces,rest) = T.span (' ' ==) content in
    (LeadingSpaces (fromIntegral $ T.length spaces), T.copy rest)
  Just _ -> 
    (NoLeadingWS, content)

parseLine :: Text -> RIO App ParsedLine
parseLine content = 
    case result of 
      Left err -> do
        logDebug . display . T.pack $
          "Defaulting back to content line: " <> err
        return $ mkContentLine content
      Right val -> do
        logInfo . display . T.pack $
          "Parsed non-content line: " <> show val
        return val
  where
    result = (`parseOnly` content) $ 
      parseEmptyLine <|> parseEndMarker <|> parseStartMarker

snipStartT :: Text
snipStartT = "<<<@snip"

parseEmptyLine :: Parser ParsedLine
parseEmptyLine =
  skipSpace >> endOfInput >> return EmptyLine

parseStartMarker :: Parser ParsedLine
parseStartMarker = 
    parseStartMarkerBeginning >> 
      (StartMarkerLine <$> (parseLabel <|> return Nothing))
  where
    parseStartMarkerBeginning = 
      string snipStartT <|>
        (Parser.take 1 >> parseStartMarkerBeginning)

snipEndT :: Text
snipEndT = "@snip>>>"

parseEndMarker :: Parser ParsedLine
parseEndMarker = 
    parseEndMarkerBeginning >>
      (EndMarkerLine <$> (parseLabel <|> return Nothing))
  where
    parseEndMarkerBeginning = 
      string snipEndT <|>
        (Parser.take 1 >> parseEndMarkerBeginning)

parseLabel :: Parser (Maybe (Path Rel File))
parseLabel = do
  skipSpace
  lbl <- Parser.takeTill Parser.isHorizontalSpace
  return $ parseRelFile (T.unpack lbl)

run :: RIO App ()
run = ask >>= \App{appFiles} ->
  pooledMapConcurrently_ runFile appFiles

runFile :: Path Rel File -> RIO App ()
runFile relFile = ask >>= \App{appBaseDir,appOutputDir} -> do
    let inFile = appBaseDir </> relFile
    let outDirBase = appOutputDir </> relFile
    outDir <- addExtension ".snips" outDirBase >>= addExtension ".d" >>= fileToDir
    logInfo . display . T.pack $ 
      "Processing file " <> show inFile <> " into dir " <> show outDir
    processFile inFile outDir
  where
    fileToDir = parseAbsDir . toFilePath

processFile :: Path Abs File -> Path Abs Dir -> RIO App ()
processFile inFile outDir = do
  fileLines <- liftIO $ T.lines <$> readFileUtf8 (toFilePath inFile)
  fileContents <- pooledMapConcurrently parseLine fileLines
  fileEx <- (".snip" <>) <$> fileExtension inFile
  logDebug . display . T.pack $ 
    "File contents for " <> show inFile <> ": " <> show fileContents
  processContents fileEx outDir 0 fileContents

processContents :: String -> Path Abs Dir -> Word -> [ParsedLine] -> RIO App ()
processContents _ _ _ [] = return ()
processContents fileExt outDir startCount (StartMarkerLine mayLbl:rest) = do
    logDebug . display . T.pack $
      "Saw the start of snippet for " <> show outDir <> ": " <> lbl
    outFile <- (outDir </>) <$> parseRelFile (lbl <> fileExt)
    concurrently_
      (processSnip outFile lbl rest)
      (processContents fileExt outDir (startCount+1) rest)
  where
    lbl = maybe (show startCount) toFilePath mayLbl
processContents fileExt outDir startCount (_:rest) = processContents fileExt outDir startCount rest

processSnip :: Path Abs File -> String -> [ParsedLine] -> RIO App ()
processSnip outFile _ [] =
  logWarn . display . T.pack $
    "No contents for snippet for " <> show outFile
processSnip outFile lbl (EmptyLine:remainingLines) =
  processSnip outFile lbl remainingLines
processSnip outFile lbl remainingLines = do
  contentT <- T.dropWhile ('\n' ==) . T.dropWhileEnd ('\n' ==) . T.unlines <$> extractSnipContent lbl remainingLines
  ensureDir $ parent outFile
  logInfo . display . T.pack $
    "Snippet for " <> show outFile <> ": " <> T.unpack contentT
  writeFileUtf8 (toFilePath outFile) contentT

extractSnipContent :: String -> [ParsedLine] -> RIO App [Text]
extractSnipContent _ [] = return []
extractSnipContent _ [EmptyLine] = return []
extractSnipContent lbl (EmptyLine:rest) = 
  ("" :) <$> extractSnipContent lbl rest
extractSnipContent _ (EndMarkerLine Nothing:_) = return []
extractSnipContent lbl (EndMarkerLine (Just endLbl):rest)
  | lbl == toFilePath endLbl = return []
  | otherwise = extractSnipContent lbl rest
extractSnipContent lbl (StartMarkerLine _:rest) = extractSnipContent lbl rest
extractSnipContent lbl parsedLines@(ContentLine _:_) = do
    App{appDoTrim} <- ask
    (contentLines appDoTrim <>) <$> extractSnipContent lbl rest
  where
    contentLines doTrim = reduceContent doTrim commonLeadingWS <$> contentSpecs
    reduceContent False _ (lineLeadWS, content) = expandWS lineLeadWS <> content
    reduceContent True comLeadWS (lineLeadWS, content) =
      case (comLeadWS, lineLeadWS) of
        (_,NoLeadingWS) -> content
        (NoLeadingWS,_) -> expandWS lineLeadWS <> content
        (LeadingSpaces _, LeadingTabs _) -> content
        (LeadingTabs _, LeadingSpaces _) -> content
        (LeadingSpaces comCnt, LeadingSpaces lineCnt) ->
          expandWS (LeadingSpaces $ lineCnt - min lineCnt comCnt) <> content
        (LeadingTabs comCnt, LeadingTabs lineCnt) ->
          expandWS (LeadingTabs $ lineCnt - min lineCnt comCnt) <> content
    expandWS = \case
      NoLeadingWS -> ""
      LeadingSpaces cnt -> T.pack $ L.replicate (fromIntegral cnt) ' '
      LeadingTabs cnt -> T.pack $ L.replicate (fromIntegral cnt) '\t'
    commonLeadingWS = fromMaybe NoLeadingWS $
      foldr 
        (\item memo ->
          case (item,memo) of
            ((NoLeadingWS,content), _) -> 
              if T.null content then
                memo
              else 
                Just NoLeadingWS
            (_, Just NoLeadingWS) -> Just NoLeadingWS
            ((lead,_), Nothing) -> Just lead
            ((LeadingTabs lineCnt,_), Just (LeadingTabs comCnt)) ->
              Just (LeadingTabs $ min lineCnt comCnt)
            ((LeadingSpaces lineCnt,_), Just (LeadingSpaces comCnt)) ->
              Just (LeadingSpaces $ min lineCnt comCnt)
            ((LeadingTabs _,_), Just (LeadingSpaces _)) ->
              Just NoLeadingWS
            ((LeadingSpaces _,_), Just (LeadingTabs _)) ->
              Just NoLeadingWS
        )
        Nothing
        contentSpecs
    contentSpecs = catMaybes $ mayContent <$> contentParsedLines
    (contentParsedLines, rest) = L.span isContentLine parsedLines
    mayContent = \case
        EmptyLine -> Just (NoLeadingWS, "")
        ContentLine spec -> Just spec
        _ -> Nothing
    isContentLine = isJust . mayContent