packages feed

scrod 1.0.0.0 → 1.1.0.0

raw patch · 8 files changed

+385/−3 lines, 8 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Scrod.Convert.FromGhc.Doc: isContinuationLine :: String -> Bool
+ Scrod.Convert.FromGhc.Doc: moduleHeaderFieldNames :: [String]
+ Scrod.Convert.FromGhc.Doc: parseFieldHeader :: String -> Maybe (String, String)
+ Scrod.Convert.FromGhc.Doc: parseFieldLines :: [String] -> ([(String, String)], [String])
+ Scrod.Convert.FromGhc.Doc: parseModuleDoc :: String -> (Doc, Maybe Since)
+ Scrod.Convert.FromGhc.Doc: parseModuleHeaderFields :: String -> ([(String, String)], String)
+ Scrod.Convert.FromGhc.Doc: trimValue :: String -> String
+ Scrod.Convert.FromHaddock: flattenAppend :: [Doc] -> [Doc]
+ Scrod.Convert.FromHaddock: groupCollapsible :: Doc -> Doc
+ Scrod.Convert.FromHaddock: groupHeaders :: [Doc] -> [Doc]
+ Scrod.Convert.FromHaddock: isHeaderAtOrAbove :: Level -> Doc -> Bool
+ Scrod.Convert.FromHaddock: mapDocChildren :: (Doc -> Doc) -> Doc -> Doc
+ Scrod.Core.Collapsible: MkCollapsible :: Header doc -> doc -> Collapsible doc
+ Scrod.Core.Collapsible: [body] :: Collapsible doc -> doc
+ Scrod.Core.Collapsible: [header] :: Collapsible doc -> Header doc
+ Scrod.Core.Collapsible: data Collapsible doc
+ Scrod.Core.Collapsible: instance GHC.Internal.Classes.Eq doc => GHC.Internal.Classes.Eq (Scrod.Core.Collapsible.Collapsible doc)
+ Scrod.Core.Collapsible: instance GHC.Internal.Classes.Ord doc => GHC.Internal.Classes.Ord (Scrod.Core.Collapsible.Collapsible doc)
+ Scrod.Core.Collapsible: instance GHC.Internal.Generics.Generic (Scrod.Core.Collapsible.Collapsible doc)
+ Scrod.Core.Collapsible: instance GHC.Internal.Show.Show doc => GHC.Internal.Show.Show (Scrod.Core.Collapsible.Collapsible doc)
+ Scrod.Core.Collapsible: instance Scrod.Json.ToJson.ToJson doc => Scrod.Json.ToJson.ToJson (Scrod.Core.Collapsible.Collapsible doc)
+ Scrod.Core.Collapsible: instance Scrod.Schema.ToSchema doc => Scrod.Schema.ToSchema (Scrod.Core.Collapsible.Collapsible doc)
+ Scrod.Core.Doc: CollapsibleHeader :: Collapsible Doc -> Doc

Files

scrod.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.12 name: scrod-version: 1.0.0.0+version: 1.1.0.0 synopsis: Worse Haskell documentation description:   Scrod generates documentation for Haskell modules, similar to Haddock. Unlike@@ -114,6 +114,7 @@     Scrod.Convert.ToHtml     Scrod.Convert.ToJsonSchema     Scrod.Core.Category+    Scrod.Core.Collapsible     Scrod.Core.Column     Scrod.Core.Definition     Scrod.Core.Doc
source/library/Scrod/Convert/FromGhc.hs view
@@ -171,7 +171,7 @@   SrcLoc.Located (Syntax.HsModule Ghc.GhcPs) ->   (Doc.Doc, Maybe Since.Since) extractModuleDocAndSince lHsModule =-  maybe (Doc.Empty, Nothing) GhcDoc.parseDoc (extractRawDocString lHsModule)+  maybe (Doc.Empty, Nothing) GhcDoc.parseModuleDoc (extractRawDocString lHsModule)  -- | Extract raw documentation string from the module header. extractRawDocString ::
source/library/Scrod/Convert/FromGhc/Doc.hs view
@@ -5,6 +5,8 @@ -- 'Doc.Doc' type via the Haddock parser. module Scrod.Convert.FromGhc.Doc where +import qualified Data.Char as Char+import qualified Data.List as List import qualified Data.Set as Set import qualified Data.Text as Text import qualified Documentation.Haddock.Parser as Haddock@@ -17,6 +19,7 @@ import qualified Language.Haskell.Syntax as Syntax import qualified Scrod.Convert.FromGhc.Internal as Internal import qualified Scrod.Convert.FromHaddock as FromHaddock+import qualified Scrod.Core.Definition as Definition import qualified Scrod.Core.Doc as Doc import qualified Scrod.Core.Since as Since @@ -44,6 +47,96 @@       doc = FromHaddock.fromHaddock $ Haddock._doc metaDoc       itemSince = Haddock._metaSince (Haddock._meta metaDoc) >>= Internal.metaSinceToSince    in (doc, itemSince)++-- | Parse module documentation, extracting header fields as a definition list.+--+-- Haddock module comments may begin with metadata fields like+-- @Description@, @Copyright@, @License@, etc. This function strips+-- those fields from the input, parses the remaining text with+-- 'parseDoc', and prepends the fields as a 'Doc.DefList'.+parseModuleDoc :: String -> (Doc.Doc, Maybe Since.Since)+parseModuleDoc input =+  let (fields, rest) = parseModuleHeaderFields input+      (doc, since) = parseDoc rest+      fieldsDoc = case fields of+        [] -> Doc.Empty+        _ ->+          Doc.DefList $+            fmap+              ( \(name, value) ->+                  Definition.MkDefinition+                    { Definition.term = Doc.String $ Text.pack name,+                      Definition.definition = Doc.String . Text.pack $ trimValue value+                    }+              )+              fields+   in (Internal.appendDoc fieldsDoc doc, since)++-- | Trim leading and trailing whitespace from a field value.+trimValue :: String -> String+trimValue = List.dropWhileEnd Char.isSpace . dropWhile Char.isSpace++-- | Parse Haddock module header fields from a doc string.+--+-- Returns the list of @(fieldName, fieldValue)@ pairs and the+-- remaining doc string after the fields section.+parseModuleHeaderFields :: String -> ([(String, String)], String)+parseModuleHeaderFields input =+  let ls = lines input+      (blanks, nonBlanks) = span (all Char.isSpace) ls+      (fieldLines, restLines) = parseFieldLines nonBlanks+   in case fieldLines of+        [] -> ([], input)+        _ -> (fieldLines, unlines (blanks <> restLines))++-- | Known Haddock module header field names.+moduleHeaderFieldNames :: [String]+moduleHeaderFieldNames =+  [ "Module",+    "Description",+    "Copyright",+    "License",+    "Maintainer",+    "Stability",+    "Portability"+  ]++-- | Parse field lines from the beginning of a doc string.+--+-- Each field is @FieldName: value@ possibly followed by indented+-- continuation lines. Parsing stops at the first line that is not+-- a known field header or a continuation of the previous field.+parseFieldLines :: [String] -> ([(String, String)], [String])+parseFieldLines [] = ([], [])+parseFieldLines allLines@(l : rest) =+  case parseFieldHeader l of+    Just (name, value) ->+      let (continuations, remaining) = span isContinuationLine rest+          fullValue = List.intercalate "\n" (value : continuations)+          (moreFields, finalRest) = parseFieldLines remaining+       in ((name, fullValue) : moreFields, finalRest)+    Nothing -> ([], allLines)++-- | Try to parse a line as a field header (@FieldName: value@).+--+-- Allows optional whitespace between the field name and the colon,+-- e.g. both @Description: foo@ and @Description : foo@.+parseFieldHeader :: String -> Maybe (String, String)+parseFieldHeader line =+  let stripped = dropWhile Char.isSpace line+      lowered = fmap Char.toLower stripped+   in case List.find (\name -> fmap Char.toLower name `List.isPrefixOf` lowered) moduleHeaderFieldNames of+        Just name ->+          let afterName = dropWhile Char.isSpace $ drop (length name) stripped+           in case afterName of+                ':' : after -> Just (name, after)+                _ -> Nothing+        Nothing -> Nothing++-- | A continuation line starts with whitespace and is not blank.+isContinuationLine :: String -> Bool+isContinuationLine [] = False+isContinuationLine (c : cs) = Char.isSpace c && not (all Char.isSpace cs)  -- | Associate documentation comments with their target declarations. --
source/library/Scrod/Convert/FromHaddock.hs view
@@ -12,6 +12,7 @@ import qualified Data.Void as Void import qualified Documentation.Haddock.Parser as Haddock import qualified Documentation.Haddock.Types as Haddock+import qualified Scrod.Core.Collapsible as Collapsible import qualified Scrod.Core.Definition as Definition import qualified Scrod.Core.Doc as Doc import qualified Scrod.Core.Example as Example@@ -29,7 +30,7 @@ import qualified Scrod.Spec as Spec  fromHaddock :: Haddock.DocH Void.Void Haddock.Identifier -> Doc.Doc-fromHaddock = convertDoc . Haddock.overIdentifier convertIdentifier+fromHaddock = groupCollapsible . convertDoc . Haddock.overIdentifier convertIdentifier  convertIdentifier :: Haddock.Namespace -> String -> Maybe Identifier.Identifier convertIdentifier ns str =@@ -123,6 +124,61 @@       TableCell.contents = convertDoc $ Haddock.tableCellContents cell     } +-- | Post-processes a 'Doc.Doc' tree to detect collapsible headers.+-- A collapsible header is a 'Doc.Header' whose title is wrapped in+-- 'Doc.Bold'. The collapsible section extends until a header of equal+-- or higher level (smaller level number), or the end of the list.+groupCollapsible :: Doc.Doc -> Doc.Doc+groupCollapsible doc = case doc of+  Doc.Append xs -> case groupHeaders $ flattenAppend xs of+    [single] -> single+    grouped -> Doc.Append grouped+  _ -> mapDocChildren groupCollapsible doc++-- | Apply a function to all immediate 'Doc.Doc' children of a node.+mapDocChildren :: (Doc.Doc -> Doc.Doc) -> Doc.Doc -> Doc.Doc+mapDocChildren f doc = case doc of+  Doc.Paragraph x -> Doc.Paragraph $ f x+  Doc.Emphasis x -> Doc.Emphasis $ f x+  Doc.Monospaced x -> Doc.Monospaced $ f x+  Doc.Bold x -> Doc.Bold $ f x+  Doc.UnorderedList xs -> Doc.UnorderedList $ fmap f xs+  Doc.OrderedList xs -> Doc.OrderedList $ fmap (\ni -> ni {NumberedItem.item = f $ NumberedItem.item ni}) xs+  Doc.DefList xs -> Doc.DefList $ fmap (\d -> d {Definition.term = f $ Definition.term d, Definition.definition = f $ Definition.definition d}) xs+  Doc.CodeBlock x -> Doc.CodeBlock $ f x+  Doc.Header h -> Doc.Header h {Header.title = f $ Header.title h}+  Doc.CollapsibleHeader c -> Doc.CollapsibleHeader c {Collapsible.header = (Collapsible.header c) {Header.title = f . Header.title $ Collapsible.header c}, Collapsible.body = f $ Collapsible.body c}+  other -> other++flattenAppend :: [Doc.Doc] -> [Doc.Doc]+flattenAppend = concatMap go+  where+    go (Doc.Append xs) = flattenAppend xs+    go x = [groupCollapsible x]++groupHeaders :: [Doc.Doc] -> [Doc.Doc]+groupHeaders [] = []+groupHeaders (Doc.Header h : rest)+  | Doc.Bold title <- Header.title h =+      let level = Header.level h+          (body, remaining) = break (isHeaderAtOrAbove level) rest+          groupedBody = groupHeaders body+       in Doc.CollapsibleHeader+            Collapsible.MkCollapsible+              { Collapsible.header = h {Header.title = title},+                Collapsible.body = case groupedBody of+                  [] -> Doc.Empty+                  [single] -> single+                  multiple -> Doc.Append multiple+              }+            : groupHeaders remaining+groupHeaders (x : rest) = x : groupHeaders rest++isHeaderAtOrAbove :: Level.Level -> Doc.Doc -> Bool+isHeaderAtOrAbove level doc = case doc of+  Doc.Header h -> Header.level h <= level+  _ -> False+ spec :: (Applicative m, Monad n) => Spec.Spec m n -> n () spec s = do   Spec.named s 'fromHaddock $ do@@ -257,6 +313,20 @@       let input :: Haddock.DocH Void.Void Haddock.Identifier           input = Haddock.DocHeader Haddock.Header {Haddock.headerLevel = 2, Haddock.headerTitle = Haddock.DocString "Section"}       let expected = Doc.Header Header.MkHeader {Header.level = Level.Two, Header.title = Doc.String $ Text.pack "Section"}+      Spec.assertEq s (fromHaddock input) expected++    Spec.it s "works with collapsible header" $ do+      let input :: Haddock.DocH Void.Void Haddock.Identifier+          input =+            Haddock.DocAppend+              (Haddock.DocHeader Haddock.Header {Haddock.headerLevel = 2, Haddock.headerTitle = Haddock.DocBold (Haddock.DocString "Examples:")})+              (Haddock.DocParagraph (Haddock.DocString "content"))+      let expected =+            Doc.CollapsibleHeader+              Collapsible.MkCollapsible+                { Collapsible.header = Header.MkHeader {Header.level = Level.Two, Header.title = Doc.String $ Text.pack "Examples:"},+                  Collapsible.body = Doc.Paragraph . Doc.String $ Text.pack "content"+                }       Spec.assertEq s (fromHaddock input) expected      Spec.it s "works with table" $ do
source/library/Scrod/Convert/ToHtml.hs view
@@ -13,6 +13,7 @@ import qualified Data.Maybe as Maybe import qualified Data.Text as Text import qualified Scrod.Core.Category as Category+import qualified Scrod.Core.Collapsible as Collapsible import qualified Scrod.Core.Column as Column import qualified Scrod.Core.Definition as Definition import qualified Scrod.Core.Doc as Doc@@ -705,6 +706,7 @@   Doc.Property x -> [propertyContent x]   Doc.Examples xs -> exampleContent <$> NonEmpty.toList xs   Doc.Header x -> [headerContent x]+  Doc.CollapsibleHeader x -> [collapsibleHeaderContent x]   Doc.Table x -> [tableContent x]  definitionContents :: Definition.Definition Doc.Doc -> [Content.Content Element.Element]@@ -857,6 +859,29 @@ headerContent :: Header.Header Doc.Doc -> Content.Content Element.Element headerContent x =   element (levelToName $ Header.level x) [] . docContents $ Header.title x++collapsibleHeaderContent :: Collapsible.Collapsible Doc.Doc -> Content.Content Element.Element+collapsibleHeaderContent x =+  let header = Collapsible.header x+      lvl = Header.level header+   in element+        "details"+        []+        [ element+            "summary"+            []+            [element "span" [("role", "heading"), ("aria-level", levelToNumber lvl), ("class", levelToName lvl)] . docContents $ Header.title header],+          element "div" [] $ docContents (Collapsible.body x)+        ]++levelToNumber :: Level.Level -> String+levelToNumber x = case x of+  Level.One -> "1"+  Level.Two -> "2"+  Level.Three -> "3"+  Level.Four -> "4"+  Level.Five -> "5"+  Level.Six -> "6"  levelToName :: Level.Level -> String levelToName x = case x of
+ source/library/Scrod/Core/Collapsible.hs view
@@ -0,0 +1,19 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingVia #-}++module Scrod.Core.Collapsible where++import qualified GHC.Generics as Generics+import qualified Scrod.Core.Header as Header+import qualified Scrod.Json.ToJson as ToJson+import qualified Scrod.Schema as Schema++-- | A collapsible section with a header and body content.+-- Corresponds to Haddock's collapsible headers, where the header title+-- is wrapped in bold syntax (@__title__@).+data Collapsible doc = MkCollapsible+  { header :: Header.Header doc,+    body :: doc+  }+  deriving (Eq, Generics.Generic, Ord, Show)+  deriving (ToJson.ToJson, Schema.ToSchema) via Generics.Generically (Collapsible doc)
source/library/Scrod/Core/Doc.hs view
@@ -6,6 +6,7 @@ import qualified Data.List.NonEmpty as NonEmpty import qualified Data.Text as Text import qualified GHC.Generics as Generics+import qualified Scrod.Core.Collapsible as Collapsible import qualified Scrod.Core.Definition as Definition import qualified Scrod.Core.Example as Example import qualified Scrod.Core.Header as Header@@ -41,6 +42,7 @@   | Property Text.Text   | Examples (NonEmpty.NonEmpty Example.Example)   | Header (Header.Header Doc)+  | CollapsibleHeader (Collapsible.Collapsible Doc)   | Table (Table.Table Doc)   deriving (Eq, Generics.Generic, Ord, Show)   deriving (ToJson.ToJson, Schema.ToSchema) via Generics.Generically Doc
source/library/Scrod/TestSuite/Integration.hs view
@@ -555,6 +555,70 @@           ("/documentation/value/level", "3")         ] +    Spec.it s "works with a collapsible header" $ do+      check+        s+        """+        -- |+        -- == __Examples:__+        -- content+        module M where+        """+        [ ("/documentation/type", "\"CollapsibleHeader\""),+          ("/documentation/value/header/level", "2"),+          ("/documentation/value/header/title/type", "\"String\""),+          ("/documentation/value/header/title/value", "\"Examples:\""),+          ("/documentation/value/body/type", "\"Paragraph\"")+        ]++    Spec.it s "works with nested collapsible headers" $ do+      check+        s+        """+        -- |+        -- = __a__+        --+        -- b+        --+        -- == __c__+        --+        -- d+        module M where+        """+        [ ("/documentation/type", "\"CollapsibleHeader\""),+          ("/documentation/value/header/level", "1"),+          ("/documentation/value/header/title/type", "\"String\""),+          ("/documentation/value/header/title/value", "\"a\""),+          ("/documentation/value/body/type", "\"Append\""),+          ("/documentation/value/body/value/1/type", "\"CollapsibleHeader\""),+          ("/documentation/value/body/value/1/value/header/level", "2"),+          ("/documentation/value/body/value/1/value/header/title/type", "\"String\""),+          ("/documentation/value/body/value/1/value/header/title/value", "\"c\"")+        ]++    Spec.it s "escapes collapsible header with a larger header" $ do+      check+        s+        """+        -- |+        -- == __a__+        --+        -- b+        --+        -- = c+        --+        -- d+        module M where+        """+        [ ("/documentation/type", "\"Append\""),+          ("/documentation/value/0/type", "\"CollapsibleHeader\""),+          ("/documentation/value/0/value/header/level", "2"),+          ("/documentation/value/0/value/header/title/value", "\"a\""),+          ("/documentation/value/0/value/body/type", "\"Paragraph\""),+          ("/documentation/value/1/type", "\"Header\""),+          ("/documentation/value/1/value/level", "1")+        ]+     Spec.it s "works with a table" $ do       check         s@@ -598,6 +662,114 @@           ("/documentation/value/bodyRows/1/1/rowspan", "1"),           ("/documentation/value/bodyRows/1/1/contents/type", "\"String\""),           ("/documentation/value/bodyRows/1/1/contents/value", "\"g\"")+        ]++  Spec.describe s "module description fields" $ do+    Spec.it s "works with a single field" $ do+      check+        s+        """+        {-|+        Description: some text+        -}+        module M where+        """+        [ ("/documentation/type", "\"DefList\""),+          ("/documentation/value/0/term/type", "\"String\""),+          ("/documentation/value/0/term/value", "\"Description\""),+          ("/documentation/value/0/definition/type", "\"String\""),+          ("/documentation/value/0/definition/value", "\"some text\"")+        ]++    Spec.it s "works with all fields" $ do+      check+        s+        """+        {-|+        Module: a+        Description: b+        Copyright: c+        License: d+        Maintainer: e+        Stability: f+        Portability: g+        -}+        module M where+        """+        [ ("/documentation/type", "\"DefList\""),+          ("/documentation/value/0/term/value", "\"Module\""),+          ("/documentation/value/0/definition/value", "\"a\""),+          ("/documentation/value/6/term/value", "\"Portability\""),+          ("/documentation/value/6/definition/value", "\"g\"")+        ]++    Spec.it s "works with fields followed by documentation" $ do+      check+        s+        """+        {-|+        Description: hello++        Some docs.+        -}+        module M where+        """+        [ ("/documentation/type", "\"Append\""),+          ("/documentation/value/0/type", "\"DefList\""),+          ("/documentation/value/0/value/0/term/value", "\"Description\""),+          ("/documentation/value/0/value/0/definition/value", "\"hello\""),+          ("/documentation/value/1/type", "\"Paragraph\""),+          ("/documentation/value/1/value/type", "\"String\""),+          ("/documentation/value/1/value/value", "\"Some docs.\"")+        ]++    Spec.it s "works with case-insensitive field names" $ do+      check+        s+        """+        {-|+        description: hello+        LICENSE: BSD3+        -}+        module M where+        """+        [ ("/documentation/type", "\"DefList\""),+          ("/documentation/value/0/term/value", "\"Description\""),+          ("/documentation/value/0/definition/value", "\"hello\""),+          ("/documentation/value/1/term/value", "\"License\""),+          ("/documentation/value/1/definition/value", "\"BSD3\"")+        ]++    Spec.it s "works with space before colon" $ do+      check+        s+        """+        {-|+        Description : hello+        -}+        module M where+        """+        [ ("/documentation/type", "\"DefList\""),+          ("/documentation/value/0/term/value", "\"Description\""),+          ("/documentation/value/0/definition/value", "\"hello\"")+        ]++    Spec.it s "works with multi-line field values" $ do+      check+        s+        """+        {-|+        Copyright: (c) Someone, 2023+                   Someone Else, 2024+        License: BSD3+        -}+        module M where+        """+        [ ("/documentation/type", "\"DefList\""),+          ("/documentation/value/0/term/value", "\"Copyright\""),+          ("/documentation/value/0/definition/value", "\"(c) Someone, 2023\\n           Someone Else, 2024\""),+          ("/documentation/value/1/term/value", "\"License\""),+          ("/documentation/value/1/definition/value", "\"BSD3\"")         ]    Spec.describe s "since" $ do