Skip to content

Commit 44cd176

Browse files
authored
Better support for multiline literals (fixes #623)
1 parent 1f869d6 commit 44cd176

File tree

11 files changed

+223
-147
lines changed

11 files changed

+223
-147
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ jobs:
3333
- '9.12'
3434
include:
3535
- os: macos-latest
36-
ghc: '9.10'
36+
ghc: '9.12'
3737
- os: windows-latest
38-
ghc: '9.10'
38+
ghc: '9.12'
3939
steps:
4040
- uses: actions/checkout@v5
4141
- uses: hspec/setup-haskell@v1

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## Changes in 0.39.0
2+
- Handle multi-line values for the `description` field of `flags` (see #623) and other fields
3+
14
## Changes in 0.38.3
25
- Accept a list for `category` (see #624)
36

hpack.cabal

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
spec-version: 0.36.0
22
name: hpack
3-
version: 0.38.3
3+
version: 0.39.0
44
synopsis: A modern format for Haskell packages
55
description: See README at <https://github.com/sol/hpack#readme>
66
author: Simon Hengel <sol@typeful.net>

src/Hpack/Render.hs

Lines changed: 13 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ module Hpack.Render (
3131
, renderFlag
3232
, renderSourceRepository
3333
, renderDirectories
34-
, formatDescription
3534
#endif
3635
) where
3736

@@ -46,7 +45,7 @@ import Control.Monad.Reader
4645
import Hpack.Util
4746
import Hpack.Config
4847
import Hpack.Render.Hints
49-
import Hpack.Render.Dsl hiding (sortFieldsBy)
48+
import Hpack.Render.Dsl hiding (RenderSettings(..), defaultRenderSettings, sortFieldsBy)
5049
import qualified Hpack.Render.Dsl as Dsl
5150

5251
data RenderEnv = RenderEnv {
@@ -65,18 +64,24 @@ getPackageName = asks renderEnvPackageName
6564
renderPackage :: [String] -> Package -> String
6665
renderPackage oldCabalFile = renderPackageWith settings headerFieldsAlignment formattingHintsFieldOrder formattingHintsSectionsFieldOrder
6766
where
68-
FormattingHints{..} = sniffFormattingHints oldCabalFile
67+
hints@FormattingHints{..} = sniffFormattingHints oldCabalFile
6968
headerFieldsAlignment = fromMaybe 16 formattingHintsAlignment
70-
settings = formattingHintsRenderSettings
69+
settings = formattingHintsRenderSettings hints
7170

7271
renderPackageWith :: RenderSettings -> Alignment -> [String] -> [(String, [String])] -> Package -> String
73-
renderPackageWith settings headerFieldsAlignment existingFieldOrder sectionsFieldOrder Package{..} = intercalate "\n" (unlines header : chunks)
72+
renderPackageWith RenderSettings{..} headerFieldsAlignment existingFieldOrder sectionsFieldOrder Package{..} = intercalate "\n" (unlines header : chunks)
7473
where
74+
settings :: Dsl.RenderSettings
75+
settings = Dsl.RenderSettings {
76+
renderSettingsEmptyLinesAsDot = packageCabalVersion < makeCabalVersion [3]
77+
, ..
78+
}
79+
7580
chunks :: [String]
7681
chunks = map unlines . filter (not . null) . map (render settings 0) $ sortStanzaFields sectionsFieldOrder stanzas
7782

7883
header :: [String]
79-
header = concatMap (render settings {renderSettingsFieldAlignment = headerFieldsAlignment} 0) packageFields
84+
header = concatMap (render settings {Dsl.renderSettingsFieldAlignment = headerFieldsAlignment} 0) packageFields
8085

8186
packageFields :: [Element]
8287
packageFields = addVerbatim packageVerbatim . sortFieldsBy existingFieldOrder $
@@ -117,7 +122,7 @@ renderPackageWith settings headerFieldsAlignment existingFieldOrder sectionsFiel
117122
("name", Just packageName)
118123
, ("version", Just packageVersion)
119124
, ("synopsis", packageSynopsis)
120-
, ("description", (formatDescription packageCabalVersion headerFieldsAlignment <$> packageDescription))
125+
, ("description", packageDescription)
121126
, formatList "category" packageCategory
122127
, ("stability", packageStability)
123128
, ("homepage", packageHomepage)
@@ -139,9 +144,7 @@ renderPackageWith settings headerFieldsAlignment existingFieldOrder sectionsFiel
139144
formatValues values = guard (not $ null values) >> (Just $ intercalate separator values)
140145
where
141146
separator :: String
142-
separator = ",\n" ++ replicate n ' '
143-
where
144-
Alignment n = max headerFieldsAlignment (Alignment $ length field + 2)
147+
separator = ",\n"
145148

146149
sortStanzaFields :: [(String, [String])] -> [Element] -> [Element]
147150
sortStanzaFields sectionsFieldOrder = go
@@ -151,33 +154,6 @@ sortStanzaFields sectionsFieldOrder = go
151154
Stanza name fields : xs | Just fieldOrder <- lookup name sectionsFieldOrder -> Stanza name (sortFieldsBy fieldOrder fields) : go xs
152155
x : xs -> x : go xs
153156

154-
formatDescription :: CabalVersion -> Alignment -> String -> String
155-
formatDescription cabalVersion (Alignment alignment) description = case map emptyLineToDot $ lines description of
156-
x : xs -> intercalate "\n" (x : indent xs)
157-
[] -> ""
158-
where
159-
n :: Int
160-
n = max alignment (length ("description: " :: String))
161-
162-
indentation :: String
163-
indentation = replicate n ' '
164-
165-
emptyLineToDot :: String -> String
166-
emptyLineToDot xs
167-
| isEmptyLine xs && cabalVersion < makeCabalVersion [3] = "."
168-
| otherwise = xs
169-
170-
indent :: [String] -> [String]
171-
indent = map indentLine
172-
173-
indentLine :: String -> String
174-
indentLine xs
175-
| isEmptyLine xs = ""
176-
| otherwise = indentation ++ xs
177-
178-
isEmptyLine :: String -> Bool
179-
isEmptyLine = all isSpace
180-
181157
renderSourceRepository :: SourceRepository -> Element
182158
renderSourceRepository SourceRepository{..} = Stanza "source-repository head" [
183159
Field "type" "git"

src/Hpack/Render/Dsl.hs

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ module Hpack.Render.Dsl (
2020

2121
#ifdef TEST
2222
, Lines (..)
23+
, IndentOrAlign (..)
2324
, renderValue
2425
, addSortKey
2526
#endif
2627
) where
2728

2829
import Imports
30+
import Data.Char (isSpace)
2931

3032
data Element = Stanza String [Element] | Group Element Element | Field String Value | Verbatim String
3133
deriving (Eq, Show)
@@ -37,7 +39,26 @@ data Value =
3739
| WordList [String]
3840
deriving (Eq, Show)
3941

40-
data Lines = SingleLine String | MultipleLines [String]
42+
data Lines = SingleLine String | MultipleLines IndentOrAlign [String]
43+
deriving (Eq, Show)
44+
45+
data IndentOrAlign =
46+
Indent
47+
-- ^
48+
-- Indent lines, e.g.
49+
--
50+
-- description:
51+
-- some
52+
-- multiline
53+
-- description
54+
|
55+
Align
56+
-- ^
57+
-- Align lines with field labels, e.g.
58+
--
59+
-- description: some
60+
-- multiline
61+
-- description
4162
deriving (Eq, Show)
4263

4364
data CommaStyle = LeadingCommas | TrailingCommas
@@ -53,10 +74,11 @@ data RenderSettings = RenderSettings {
5374
renderSettingsIndentation :: Int
5475
, renderSettingsFieldAlignment :: Alignment
5576
, renderSettingsCommaStyle :: CommaStyle
77+
, renderSettingsEmptyLinesAsDot :: Bool
5678
} deriving (Eq, Show)
5779

5880
defaultRenderSettings :: RenderSettings
59-
defaultRenderSettings = RenderSettings 2 0 LeadingCommas
81+
defaultRenderSettings = RenderSettings 2 0 LeadingCommas True
6082

6183
render :: RenderSettings -> Nesting -> Element -> [String]
6284
render settings nesting = \ case
@@ -69,31 +91,55 @@ renderElements :: RenderSettings -> Nesting -> [Element] -> [String]
6991
renderElements settings nesting = concatMap (render settings nesting)
7092

7193
renderField :: RenderSettings -> String -> Value -> [String]
72-
renderField settings@RenderSettings{..} name value = case renderValue settings value of
94+
renderField settings@RenderSettings{..} name = renderValue settings >>> \ case
7395
SingleLine "" -> []
74-
SingleLine x -> [name ++ ": " ++ padding ++ x]
75-
MultipleLines [] -> []
76-
MultipleLines xs -> (name ++ ":") : map (indent settings 1) xs
96+
SingleLine value -> [fieldName ++ value]
97+
MultipleLines _ [] -> []
98+
MultipleLines Indent values -> (name ++ ":") : map (indent settings 1) values
99+
MultipleLines Align (value : values) -> (fieldName ++ value) : map align values
77100
where
78101
Alignment fieldAlignment = renderSettingsFieldAlignment
79-
padding = replicate (fieldAlignment - length name - 2) ' '
102+
103+
fieldName :: String
104+
fieldName = name ++ ": " ++ fieldNamePadding
105+
106+
fieldNamePadding :: String
107+
fieldNamePadding = replicate (fieldAlignment - length name - 2) ' '
108+
109+
align :: String -> String
110+
align = \ case
111+
"" -> ""
112+
value -> padding ++ value
113+
114+
padding :: String
115+
padding = replicate (length fieldName) ' '
80116

81117
renderValue :: RenderSettings -> Value -> Lines
82-
renderValue RenderSettings{..} v = case v of
83-
Literal s -> SingleLine s
118+
renderValue RenderSettings{..} = \ case
119+
Literal string -> case lines string of
120+
[value] -> SingleLine value
121+
values -> MultipleLines Align $ map emptyLineToDot values
84122
WordList ws -> SingleLine $ unwords ws
85123
LineSeparatedList xs -> renderLineSeparatedList renderSettingsCommaStyle xs
86124
CommaSeparatedList xs -> renderCommaSeparatedList renderSettingsCommaStyle xs
125+
where
126+
emptyLineToDot :: String -> String
127+
emptyLineToDot xs
128+
| isEmptyLine xs && renderSettingsEmptyLinesAsDot = "."
129+
| otherwise = xs
130+
131+
isEmptyLine :: String -> Bool
132+
isEmptyLine = all isSpace
87133

88134
renderLineSeparatedList :: CommaStyle -> [String] -> Lines
89-
renderLineSeparatedList style = MultipleLines . map (padding ++)
135+
renderLineSeparatedList style = MultipleLines Indent . map (padding ++)
90136
where
91137
padding = case style of
92138
LeadingCommas -> " "
93139
TrailingCommas -> ""
94140

95141
renderCommaSeparatedList :: CommaStyle -> [String] -> Lines
96-
renderCommaSeparatedList style = MultipleLines . case style of
142+
renderCommaSeparatedList style = MultipleLines Indent . case style of
97143
LeadingCommas -> map renderLeadingComma . zip (True : repeat False)
98144
TrailingCommas -> map renderTrailingComma . reverse . zip (True : repeat False) . reverse
99145
where
@@ -111,7 +157,9 @@ instance IsString Value where
111157
fromString = Literal
112158

113159
indent :: RenderSettings -> Nesting -> String -> String
114-
indent RenderSettings{..} (Nesting nesting) s = replicate (nesting * renderSettingsIndentation) ' ' ++ s
160+
indent RenderSettings{..} (Nesting nesting) = \ case
161+
"" -> ""
162+
s -> replicate (nesting * renderSettingsIndentation) ' ' ++ s
115163

116164
sortFieldsBy :: [String] -> [Element] -> [Element]
117165
sortFieldsBy existingFieldOrder =

src/Hpack/Render/Hints.hs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
{-# LANGUAGE CPP #-}
22
{-# LANGUAGE ViewPatterns #-}
3+
{-# LANGUAGE RecordWildCards #-}
34
module Hpack.Render.Hints (
45
FormattingHints (..)
56
, sniffFormattingHints
7+
, RenderSettings (..)
8+
, defaultRenderSettings
9+
, formattingHintsRenderSettings
610
#ifdef TEST
7-
, sniffRenderSettings
811
, extractFieldOrder
912
, extractSectionsFieldOrder
1013
, sanitize
@@ -21,22 +24,25 @@ import Imports
2124
import Data.Char
2225
import Data.Maybe
2326

24-
import Hpack.Render.Dsl
27+
import Hpack.Render.Dsl (Alignment(..), CommaStyle(..))
28+
import qualified Hpack.Render.Dsl as Dsl
2529
import Hpack.Util
2630

2731
data FormattingHints = FormattingHints {
2832
formattingHintsFieldOrder :: [String]
2933
, formattingHintsSectionsFieldOrder :: [(String, [String])]
3034
, formattingHintsAlignment :: Maybe Alignment
31-
, formattingHintsRenderSettings :: RenderSettings
35+
, formattingHintsIndentation :: Maybe Int
36+
, formattingHintsCommaStyle :: Maybe CommaStyle
3237
} deriving (Eq, Show)
3338

3439
sniffFormattingHints :: [String] -> FormattingHints
3540
sniffFormattingHints (sanitize -> input) = FormattingHints {
3641
formattingHintsFieldOrder = extractFieldOrder input
3742
, formattingHintsSectionsFieldOrder = extractSectionsFieldOrder input
3843
, formattingHintsAlignment = sniffAlignment input
39-
, formattingHintsRenderSettings = sniffRenderSettings input
44+
, formattingHintsIndentation = sniffIndentation input
45+
, formattingHintsCommaStyle = sniffCommaStyle input
4046
}
4147

4248
sanitize :: [String] -> [String]
@@ -124,11 +130,20 @@ sniffCommaStyle input
124130
where
125131
startsWithComma = isPrefixOf "," . dropWhile isSpace
126132

127-
sniffRenderSettings :: [String] -> RenderSettings
128-
sniffRenderSettings input = RenderSettings indentation fieldAlignment commaStyle
129-
where
130-
indentation = max def $ fromMaybe def (sniffIndentation input)
131-
where def = renderSettingsIndentation defaultRenderSettings
133+
data RenderSettings = RenderSettings {
134+
renderSettingsIndentation :: Int
135+
, renderSettingsFieldAlignment :: Alignment
136+
, renderSettingsCommaStyle :: CommaStyle
137+
} deriving (Eq, Show)
132138

133-
fieldAlignment = renderSettingsFieldAlignment defaultRenderSettings
134-
commaStyle = fromMaybe (renderSettingsCommaStyle defaultRenderSettings) (sniffCommaStyle input)
139+
defaultRenderSettings :: RenderSettings
140+
defaultRenderSettings = let Dsl.RenderSettings{..} = Dsl.defaultRenderSettings in RenderSettings{..}
141+
142+
formattingHintsRenderSettings :: FormattingHints -> RenderSettings
143+
formattingHintsRenderSettings FormattingHints{..} = defaultRenderSettings {
144+
renderSettingsIndentation = indentation
145+
, renderSettingsCommaStyle = commaStyle
146+
} where
147+
indentation = max def $ fromMaybe def formattingHintsIndentation
148+
where def = renderSettingsIndentation defaultRenderSettings
149+
commaStyle = fromMaybe (renderSettingsCommaStyle defaultRenderSettings) formattingHintsCommaStyle

test/EndToEndSpec.hs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import Data.Version (showVersion)
2020

2121
import qualified Hpack.Render as Hpack
2222
import Hpack.Config (packageConfig, readPackageConfig, DecodeOptions(..), defaultDecodeOptions, DecodeResult(..))
23-
import Hpack.Render.Hints (FormattingHints(..), sniffFormattingHints)
23+
import Hpack.Render.Hints (FormattingHints(..), sniffFormattingHints, formattingHintsRenderSettings)
2424

2525
import qualified Paths_hpack as Hpack (version)
2626

@@ -378,6 +378,26 @@ spec = around_ (inTempDirectoryNamed "my-package") $ do
378378
location: https://github.com/hspec/hspec
379379
|]
380380

381+
describe "flags" $ do
382+
it "accepts multi-line flag descriptions" $ do
383+
[i|
384+
flags:
385+
some-flag:
386+
description: |
387+
some
388+
flag
389+
description
390+
manual: True
391+
default: False
392+
|] `shouldRenderTo` package [i|
393+
flag some-flag
394+
description: some
395+
flag
396+
description
397+
manual: True
398+
default: False
399+
|]
400+
381401
describe "defaults" $ do
382402
it "accepts global defaults" $ do
383403
writeFile "defaults/sol/hpack-template/2017/defaults.yaml" [i|
@@ -2113,9 +2133,9 @@ run_ userDataDir c old = do
21132133
return $ case mPackage of
21142134
Right (DecodeResult pkg cabalVersion _ warnings) ->
21152135
let
2116-
FormattingHints{..} = sniffFormattingHints (lines old)
2136+
hints@FormattingHints{..} = sniffFormattingHints (lines old)
21172137
alignment = fromMaybe 0 formattingHintsAlignment
2118-
settings = formattingHintsRenderSettings
2138+
settings = formattingHintsRenderSettings hints
21192139
output = cabalVersion ++ Hpack.renderPackageWith settings alignment formattingHintsFieldOrder formattingHintsSectionsFieldOrder pkg
21202140
in
21212141
Right (warnings, output)

0 commit comments

Comments
 (0)