{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-| Module : Headroom.FileSupport Description : License header manipulation Copyright : (c) 2019-2020 Vaclav Svejcar License : BSD-3-Clause Maintainer : vaclav.svejcar@gmail.com Stability : experimental Portability : POSIX This module is the heart of /Headroom/ as it contains functions for working with the /license headers/ and the /source code files/. -} module Headroom.FileSupport ( -- * File info extraction extractFileInfo -- * License header manipulation , addHeader , dropHeader , replaceHeader -- * License header detection , findHeader , findBlockHeader , findLineHeader , firstMatching , lastMatching , splitInput ) where import Headroom.Configuration.Types ( CtHeaderConfig , HeaderConfig(..) , HeaderSyntax(..) ) import Headroom.Data.Lens ( suffixLensesFor ) import Headroom.Data.Regex ( Regex , match ) import Headroom.Data.TextExtra ( fromLines , toLines ) import Headroom.Ext ( extractVariables ) import Headroom.FileSupport.Types ( FileInfo(..) ) import Headroom.FileType.Types ( FileType(..) ) import Headroom.Types ( TemplateMeta(..) ) import RIO import qualified RIO.List as L import qualified RIO.Text as T suffixLensesFor ["fiHeaderPos"] ''FileInfo -- | Extracts info about the processed file to be later used by the header -- detection/manipulation functions. extractFileInfo :: FileType -- ^ type of the detected file -> CtHeaderConfig -- ^ license header configuration -> Maybe TemplateMeta -- ^ metadata extracted from /template/ -> Text -- ^ text used for detection -> FileInfo -- ^ resulting file info extractFileInfo fiFileType fiHeaderConfig meta text = let fiHeaderPos = findHeader fiHeaderConfig text fiVariables = extractVariables fiFileType fiHeaderConfig meta fiHeaderPos text in FileInfo { .. } -- | Adds given header at position specified by the 'FileInfo'. Does nothing if -- any header is already present, use 'replaceHeader' if you need to -- override it. addHeader :: FileInfo -- ^ info about file where header is added -> Text -- ^ text of the new header -> Text -- ^ text of the file where to add the header -> Text -- ^ resulting text with added header addHeader FileInfo {..} _ text | isJust fiHeaderPos = text addHeader FileInfo {..} header text = result where (before, middle, after) = splitInput hcPutAfter hcPutBefore text HeaderConfig {..} = fiHeaderConfig before' = stripLinesEnd before middle' = stripLinesStart middle margin [] _ = [] margin _ size = replicate size "" marginBefore = margin before' hcMarginBefore marginAfter = margin (middle' <> after) hcMarginAfter result = fromLines $ concat joined joined = [before', marginBefore, [header], marginAfter, middle', after] -- | Drops header at position specified by the 'FileInfo' from the given text. -- Does nothing if no header is present. dropHeader :: FileInfo -- ^ info about the file from which the header will be dropped -> Text -- ^ text of the file from which to drop the header -> Text -- ^ resulting text with dropped header dropHeader (FileInfo _ _ Nothing _) text = text dropHeader (FileInfo _ _ (Just (start, end)) _) text = result where before = take start inputLines after = drop (end + 1) inputLines inputLines = toLines text result = fromLines (stripLinesEnd before <> stripLinesStart after) -- | Replaces existing header at position specified by the 'FileInfo' in the -- given text. Basically combines 'addHeader' with 'dropHeader'. If no header -- is present, then the given one is added to the text. replaceHeader :: FileInfo -- ^ info about the file in which to replace the header -> Text -- ^ text of the new header -> Text -- ^ text of the file where to replace the header -> Text -- ^ resulting text with replaced header replaceHeader fileInfo header = addHeader' . dropHeader' where addHeader' = addHeader infoWithoutPos header dropHeader' = dropHeader fileInfo infoWithoutPos = set fiHeaderPosL Nothing fileInfo -- | Finds header position in given text, where position is represented by -- line number of first and last line of the header (numbered from zero). -- Based on the 'HeaderSyntax' specified in given 'HeaderConfig', this function -- delegates its work to either 'findBlockHeader' or 'findLineHeader'. -- -- >>> :set -XFlexibleContexts -- >>> :set -XTypeFamilies -- >>> let hc = HeaderConfig ["hs"] 0 0 [] [] (BlockComment "{-" "-}") -- >>> findHeader hc "foo\nbar\n{- HEADER -}\nbaz" -- Just (2,2) findHeader :: CtHeaderConfig -- ^ appropriate header configuration -> Text -- ^ text in which to detect the header -> Maybe (Int, Int) -- ^ header position @(startLine, endLine)@ findHeader HeaderConfig {..} input = case hcHeaderSyntax of BlockComment start end -> findBlockHeader start end inLines splitAt LineComment prefix -> findLineHeader prefix inLines splitAt where (before, headerArea, _) = splitInput hcPutAfter hcPutBefore input splitAt = L.length before inLines = T.strip <$> headerArea -- | Finds header in the form of /multi-line comment/ syntax, which is delimited -- with starting and ending pattern. -- -- >>> findBlockHeader "{-" "-}" ["", "{- HEADER -}", "", ""] 0 -- Just (1,1) findBlockHeader :: Text -- ^ starting pattern (e.g. @{-@ or @/*@) -> Text -- ^ ending pattern (e.g. @-}@ or @*/@) -> [Text] -- ^ lines of text in which to detect the header -> Int -- ^ line number offset (adds to resulting position) -> Maybe (Int, Int) -- ^ header position @(startLine + offset, endLine + offset)@ findBlockHeader startsWith endsWith = go Nothing Nothing where isStart = T.isPrefixOf startsWith isEnd = T.isSuffixOf endsWith go _ _ (x : _) i | isStart x && isEnd x = Just (i, i) go _ _ (x : xs) i | isStart x = go (Just i) Nothing xs (i + 1) go (Just start) _ (x : _) i | isEnd x = Just (start, i) go start end (_ : xs) i = go start end xs (i + 1) go _ _ [] _ = Nothing -- | Finds header in the form of /single-line comment/ syntax, which is -- delimited with the prefix pattern. -- -- >>> findLineHeader "--" ["", "a", "-- first", "-- second", "foo"] 0 -- Just (2,3) findLineHeader :: Text -- ^ prefix pattern (e.g. @--@ or @//@) -> [Text] -- ^ lines of text in which to detect the header -> Int -- ^ line number offset (adds to resulting position) -> Maybe (Int, Int) -- ^ header position @(startLine + offset, endLine + offset)@ findLineHeader prefix = go Nothing where isPrefix = T.isPrefixOf prefix go Nothing (x : xs) i | isPrefix x = go (Just i) xs (i + 1) go Nothing (_ : xs) i = go Nothing xs (i + 1) go (Just start) (x : xs) i | isPrefix x = go (Just start) xs (i + 1) go (Just start) _ i = Just (start, i - 1) go _ [] _ = Nothing -- | Finds very first line that matches the given /regex/ (numbered from zero). -- -- >>> import Headroom.Data.Regex (re) -- >>> :set -XQuasiQuotes -- >>> firstMatching [[re|^foo|]] ["some text", "foo bar", "foo baz", "last"] -- Just 1 firstMatching :: [Regex] -- ^ /regex/ used for matching -> [Text] -- ^ input lines -> Maybe Int -- ^ matching line number firstMatching patterns input = go input 0 where cond x = any (\r -> isJust $ match r x) patterns go (x : _) i | cond x = Just i go (_ : xs) i = go xs (i + 1) go [] _ = Nothing -- | Finds very last line that matches the given /regex/ (numbered from zero). -- -- >>> import Headroom.Data.Regex (re) -- >>> :set -XQuasiQuotes -- >>> lastMatching [[re|^foo|]] ["some text", "foo bar", "foo baz", "last"] -- Just 2 lastMatching :: [Regex] -- ^ /regex/ used for matching -> [Text] -- ^ input lines -> Maybe Int -- ^ matching line number lastMatching patterns input = go input 0 Nothing where cond x = any (\r -> isJust $ match r x) patterns go (x : xs) i _ | cond x = go xs (i + 1) (Just i) go (_ : xs) i pos = go xs (i + 1) pos go [] _ pos = pos -- | Splits input lines into three parts: -- -- 1. list of all lines located before the very last occurence of one of -- the conditions from the first condition list -- 2. list of all lines between the first and last lists -- 3. list of all lines located after the very first occurence of one of -- the conditions from the second condition list -- -- If both first and second patterns are empty, then all lines are returned in -- the middle list. -- -- >>> import Headroom.Data.Regex (re) -- >>> :set -XQuasiQuotes -- -- >>> splitInput [[re|->|]] [[re|<-|]] "text\n->\nRESULT\n<-\nfoo" -- (["text","->"],["RESULT"],["<-","foo"]) -- -- >>> splitInput [] [[re|<-|]] "text\n->\nRESULT\n<-\nfoo" -- ([],["text","->","RESULT"],["<-","foo"]) -- -- >>> splitInput [] [] "one\ntwo" -- ([],["one","two"],[]) splitInput :: [Regex] -- ^ patterns for first split -> [Regex] -- ^ patterns for second split -> Text -- ^ text to split -> ([Text], [Text], [Text]) -- ^ result lines as @([before1stSplit], [middle], [after2ndSplit])@ splitInput [] [] input = ([], toLines input, []) splitInput fstSplit sndSplit input = (before, middle, after) where (middle', after ) = L.splitAt sndSplitAt inLines (before , middle) = L.splitAt fstSplitAt middle' fstSplitAt = maybe 0 (+ 1) (lastMatching fstSplit middle') sndSplitAt = fromMaybe len (firstMatching sndSplit inLines) inLines = toLines input len = L.length inLines stripLinesEnd :: [Text] -> [Text] stripLinesEnd = toLines . T.stripEnd . fromLines stripLinesStart :: [Text] -> [Text] stripLinesStart = toLines . T.stripStart . fromLines