haskell-gi-0.20.1: lib/Data/GI/CodeGen/GtkDoc.hs
-- | A parser for gtk-doc formatted documentation, see
-- https://developer.gnome.org/gtk-doc-manual/ for the spec.
module Data.GI.CodeGen.GtkDoc
( parseGtkDoc
, GtkDoc(..)
, Token(..)
, Language(..)
, Link(..)
, ListItem(..)
, CRef(..)
) where
import Prelude hiding (takeWhile)
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>), (<*))
#endif
import Data.Monoid ((<>))
import Control.Applicative ((<|>))
import Data.Attoparsec.Text
import Data.Char (isAsciiUpper, isAsciiLower, isDigit)
import qualified Data.Text as T
import Data.Text (Text)
-- | A parsed gtk-doc token.
data Token = Literal Text
| Verbatim Text
| CodeBlock (Maybe Language) Text
| ExternalLink Link
| Image Link
| List [ListItem]
| SectionHeader Int GtkDoc -- ^ A section header of the given depth.
| SymbolRef CRef
deriving (Show, Eq)
-- | A link to a resource, either offline or a section of the documentation.
data Link = Link { linkName :: Text
, linkAddress :: Text }
deriving (Show, Eq)
-- | An item in a list, given by a list of lines (not including ending
-- newlines). The list is always non-empty, so we represent it by the
-- first line and then a possibly empty list with the rest of the lines.
data ListItem = ListItem GtkDoc [GtkDoc]
deriving (Show, Eq)
-- | The language for an embedded code block.
newtype Language = Language Text
deriving (Show, Eq)
-- | A reference to some symbol in the API.
data CRef = FunctionRef Text
| ParamRef Text
| ConstantRef Text
| SignalRef Text Text
| PropertyRef Text Text
| VMethodRef Text Text
| StructFieldRef Text Text
| TypeRef Text
deriving (Show, Eq, Ord)
-- | A parsed representation of gtk-doc formatted documentation.
newtype GtkDoc = GtkDoc [Token]
deriving (Show, Eq)
-- | Parse the given gtk-doc formatted documentation.
--
-- === __Examples__
-- >>> parseGtkDoc ""
-- GtkDoc []
--
-- >>> parseGtkDoc "func()"
-- GtkDoc [SymbolRef (FunctionRef "func")]
--
-- >>> parseGtkDoc "literal"
-- GtkDoc [Literal "literal"]
--
-- >>> parseGtkDoc "This is a long literal"
-- GtkDoc [Literal "This is a long literal"]
--
-- >>> parseGtkDoc "Call foo() for free cookies"
-- GtkDoc [Literal "Call ",SymbolRef (FunctionRef "foo"),Literal " for free cookies"]
--
-- >>> parseGtkDoc "The signal ##%#GtkButton::activate is related to gtk_button_activate()."
-- GtkDoc [Literal "The signal ##%",SymbolRef (SignalRef "GtkButton" "activate"),Literal " is related to ",SymbolRef (FunctionRef "gtk_button_activate"),Literal "."]
--
-- >>> parseGtkDoc "# A section\n\n## and a subsection ##\n"
-- GtkDoc [SectionHeader 1 (GtkDoc [Literal "A section"]),Literal "\n",SectionHeader 2 (GtkDoc [Literal "and a subsection "])]
--
-- >>> parseGtkDoc "Compact list:\n- First item\n- Second item"
-- GtkDoc [Literal "Compact list:\n",List [ListItem (GtkDoc [Literal "First item"]) [],ListItem (GtkDoc [Literal "Second item"]) []]]
--
-- >>> parseGtkDoc "Spaced list:\n\n- First item\n\n- Second item"
-- GtkDoc [Literal "Spaced list:\n",List [ListItem (GtkDoc [Literal "First item"]) [],ListItem (GtkDoc [Literal "Second item"]) []]]
--
-- >>> parseGtkDoc "List with urls:\n- [test](http://test)\n- "
-- GtkDoc [Literal "List with urls:\n",List [ListItem (GtkDoc [ExternalLink (Link {linkName = "test", linkAddress = "http://test"})]) [],ListItem (GtkDoc [Image (Link {linkName = "", linkAddress = "image.png"})]) []]]
parseGtkDoc :: Text -> GtkDoc
parseGtkDoc raw =
case parseOnly (parseTokens <* endOfInput) raw of
Left e ->
error $ "gtk-doc parsing failed with error \"" <> e
<> "\" on the input \"" <> T.unpack raw <> "\""
Right tks -> GtkDoc . coalesceLiterals
. restoreSHPreNewlines . restoreListPreNewline $ tks
-- | `parseSectionHeader` eats the newline before the section header,
-- but `parseInitialSectionHeader` does not, since it only matches at
-- the beginning of the text. This restores the newlines eaten by
-- `parseSectionHeader`, so a `SectionHeader` returned by the parser
-- can always be assumed /not/ to have an implicit starting newline.
restoreSHPreNewlines :: [Token] -> [Token]
restoreSHPreNewlines [] = []
restoreSHPreNewlines (i : rest) = i : restoreNewlines rest
where restoreNewlines :: [Token] -> [Token]
restoreNewlines [] = []
restoreNewlines (s@(SectionHeader _ _) : rest) =
Literal "\n" : s : restoreNewlines rest
restoreNewlines (x : rest) = x : restoreNewlines rest
-- | `parseList` eats the newline before the list, restore it.
restoreListPreNewline :: [Token] -> [Token]
restoreListPreNewline [] = []
restoreListPreNewline (l@(List _) : rest) =
Literal "\n" : l : restoreListPreNewline rest
restoreListPreNewline (x : rest) = x : restoreListPreNewline rest
-- | Accumulate consecutive literals into a single literal.
coalesceLiterals :: [Token] -> [Token]
coalesceLiterals tks = go Nothing tks
where
go :: Maybe Text -> [Token] -> [Token]
go Nothing [] = []
go (Just l) [] = [Literal l]
go Nothing (Literal l : rest) = go (Just l) rest
go (Just l) (Literal l' : rest) = go (Just (l <> l')) rest
go Nothing (tk : rest) = tk : go Nothing rest
go (Just l) (tk : rest) = Literal l : tk : go Nothing rest
-- | Parser for tokens.
parseTokens :: Parser [Token]
parseTokens = headerAndTokens <|> justTokens
where -- In case the input starts by a section header.
headerAndTokens :: Parser [Token]
headerAndTokens = do
header <- parseInitialSectionHeader
tokens <- justTokens
return (header : tokens)
justTokens :: Parser [Token]
justTokens = many' parseToken
-- | Parse a single token.
--
-- === __Examples__
-- >>> parseOnly (parseToken <* endOfInput) "func()"
-- Right (SymbolRef (FunctionRef "func"))
parseToken :: Parser Token
parseToken = -- Note that the parsers overlap, so this is not as
-- efficient as it could be (if we had combined parsers
-- and then branched, so that there is no
-- backtracking). But speed is not an issue here, so for
-- clarity we keep the parsers distinct. The exception
-- is parseFunctionRef, since it does not complicate the
-- parser much, and it is the main source of
-- backtracking.
parseFunctionRef
<|> parseSignal
<|> parseProperty
<|> parseVMethod
<|> parseStructField
<|> parseType
<|> parseConstant
<|> parseParam
<|> parseEscaped
<|> parseVerbatim
<|> parseCodeBlock
<|> parseUrl
<|> parseImage
<|> parseSectionHeader
<|> parseList
<|> parseBoringLiteral
-- | Parse a signal name, of the form
-- > #Object::signal
--
-- === __Examples__
-- >>> parseOnly (parseSignal <* endOfInput) "#GtkButton::activate"
-- Right (SymbolRef (SignalRef "GtkButton" "activate"))
parseSignal :: Parser Token
parseSignal = do
_ <- char '#'
obj <- parseCIdent
_ <- string "::"
signal <- signalOrPropName
return (SymbolRef (SignalRef obj signal))
-- | Parse a property name, of the form
-- > #Object:property
--
-- === __Examples__
-- >>> parseOnly (parseProperty <* endOfInput) "#GtkButton:always-show-image"
-- Right (SymbolRef (PropertyRef "GtkButton" "always-show-image"))
parseProperty :: Parser Token
parseProperty = do
_ <- char '#'
obj <- parseCIdent
_ <- char ':'
property <- signalOrPropName
return (SymbolRef (PropertyRef obj property))
-- | Parse a reference to a virtual method, of the form
-- > #Struct.method()
--
-- === __Examples__
-- >>> parseOnly (parseVMethod <* endOfInput) "#Foo.bar()"
-- Right (SymbolRef (VMethodRef "Foo" "bar"))
parseVMethod :: Parser Token
parseVMethod = do
_ <- char '#'
obj <- parseCIdent
_ <- char '.'
method <- parseCIdent
_ <- string "()"
return (SymbolRef (VMethodRef obj method))
-- | Parse a reference to a struct field, of the form
-- > #Struct.field
--
-- === __Examples__
-- >>> parseOnly (parseStructField <* endOfInput) "#Foo.bar"
-- Right (SymbolRef (StructFieldRef "Foo" "bar"))
parseStructField :: Parser Token
parseStructField = do
_ <- char '#'
obj <- parseCIdent
_ <- char '.'
field <- parseCIdent
return (SymbolRef (StructFieldRef obj field))
-- | Parse a reference to a C type, of the form
-- > #Type
--
-- === __Examples__
-- >>> parseOnly (parseType <* endOfInput) "#Foo"
-- Right (SymbolRef (TypeRef "Foo"))
parseType :: Parser Token
parseType = do
_ <- char '#'
obj <- parseCIdent
return (SymbolRef (TypeRef obj))
-- | Parse a constant, of the form
-- > %CONSTANT_NAME
--
-- === __Examples__
-- >>> parseOnly (parseConstant <* endOfInput) "%TEST_CONSTANT"
-- Right (SymbolRef (ConstantRef "TEST_CONSTANT"))
parseConstant :: Parser Token
parseConstant = do
_ <- char '%'
c <- parseCIdent
return (SymbolRef (ConstantRef c))
-- | Parse a reference to a parameter, of the form
-- > @param_name
--
-- === __Examples__
-- >>> parseOnly (parseParam <* endOfInput) "@test_param"
-- Right (SymbolRef (ParamRef "test_param"))
parseParam :: Parser Token
parseParam = do
_ <- char '@'
param <- parseCIdent
return (SymbolRef (ParamRef param))
-- | Whether the given character is valid in a C identifier.
isCIdent :: Char -> Bool
isCIdent '_' = True
isCIdent c = isDigit c || isAsciiUpper c || isAsciiLower c
-- | Name of a signal or property name. Similar to a C identifier, but
-- hyphens are allowed too.
signalOrPropName :: Parser Text
signalOrPropName = takeWhile1 isSignalOrPropIdent
where isSignalOrPropIdent :: Char -> Bool
isSignalOrPropIdent '-' = True
isSignalOrPropIdent c = isCIdent c
-- | Something that could be a valid C identifier (loosely speaking,
-- we do not need to be too strict here).
parseCIdent :: Parser Text
parseCIdent = takeWhile1 isCIdent
-- | Parse a function ref, given by a valid C identifier followed by
-- '()', for instance 'gtk_widget_show()'. If the identifier is not
-- followed by "()", return it as a literal instead.
--
-- === __Examples__
-- >>> parseOnly (parseFunctionRef <* endOfInput) "test_func()"
-- Right (SymbolRef (FunctionRef "test_func"))
--
-- >>> parseOnly (parseFunctionRef <* endOfInput) "not_a_func"
-- Right (Literal "not_a_func")
parseFunctionRef :: Parser Token
parseFunctionRef = do
ident <- parseCIdent
option (Literal ident) (string "()" >>
return (SymbolRef (FunctionRef ident)))
-- | Parse a escaped special character, i.e. one preceded by '\'.
parseEscaped :: Parser Token
parseEscaped = do
_ <- char '\\'
c <- satisfy (`elem` ("#@%\\`" :: [Char]))
return $ Literal (T.singleton c)
-- | Parse a literal, i.e. anything without a known special
-- meaning. Note that this parser always consumes the first character,
-- regardless of what it is.
parseBoringLiteral :: Parser Token
parseBoringLiteral = do
c <- anyChar
boring <- takeWhile (not . special)
return $ Literal (T.cons c boring)
-- | List of special characters from the point of view of the parser
-- (in the sense that they may be the beginning of something with a
-- special interpretation).
special :: Char -> Bool
special '#' = True
special '@' = True
special '%' = True
special '\\' = True
special '`' = True
special '|' = True
special '[' = True
special '!' = True
special '\n' = True
special c = isCIdent c
-- | Parse a verbatim string, of the form
-- > `verbatim text`
--
-- === __Examples__
-- >>> parseOnly (parseVerbatim <* endOfInput) "`Example quote!`"
-- Right (Verbatim "Example quote!")
parseVerbatim :: Parser Token
parseVerbatim = do
_ <- char '`'
v <- takeWhile1 (/= '`')
_ <- char '`'
return $ Verbatim v
-- | Parse a URL in Markdown syntax, of the form
-- > [name](url)
--
-- === __Examples__
-- >>> parseOnly (parseUrl <* endOfInput) "[haskell](http://haskell.org)"
-- Right (ExternalLink (Link {linkName = "haskell", linkAddress = "http://haskell.org"}))
parseUrl :: Parser Token
parseUrl = do
_ <- char '['
name <- takeWhile1 (/= ']')
_ <- string "]("
address <- takeWhile1 (/= ')')
_ <- char ')'
return $ ExternalLink $ Link {linkName = name, linkAddress = address}
-- | Parse an image reference, of the form
-- > 
--
-- === __Examples__
-- >>> parseOnly (parseImage <* endOfInput) ""
-- Right (Image (Link {linkName = "", linkAddress = "diagram.png"}))
parseImage :: Parser Token
parseImage = do
_ <- string "!["
name <- takeWhile (/= ']')
_ <- string "]("
address <- takeWhile1 (/= ')')
_ <- char ')'
return $ Image $ Link {linkName = name, linkAddress = address}
-- | Parse a code block embedded in the documentation.
parseCodeBlock :: Parser Token
parseCodeBlock = do
_ <- string "|["
lang <- (Just <$> parseLanguage) <|> return Nothing
code <- T.pack <$> manyTill anyChar (string "]|")
return $ CodeBlock lang code
-- | Parse the language of a code block, specified as a comment.
parseLanguage :: Parser Language
parseLanguage = do
_ <- string "<!--"
skipSpace
_ <- string "language=\""
lang <- takeWhile1 (/= '"')
_ <- char '"'
skipSpace
_ <- string "-->"
return $ Language lang
-- | Parse a section header, given by a number of hash symbols, and
-- then ordinary text. Note that this parser "eats" the newline before
-- and after the section header.
parseSectionHeader :: Parser Token
parseSectionHeader = char '\n' >> parseInitialSectionHeader
-- | Parse a section header at the beginning of the text. I.e. this is
-- the same as `parseSectionHeader`, but we do not expect a newline as
-- a first character.
--
-- === __Examples__
-- >>> parseOnly (parseInitialSectionHeader <* endOfInput) "### Hello! ###\n"
-- Right (SectionHeader 3 (GtkDoc [Literal "Hello! "]))
--
-- >>> parseOnly (parseInitialSectionHeader <* endOfInput) "# Hello!\n"
-- Right (SectionHeader 1 (GtkDoc [Literal "Hello!"]))
parseInitialSectionHeader :: Parser Token
parseInitialSectionHeader = do
hashes <- takeWhile1 (== '#')
_ <- many1 space
heading <- takeWhile1 (notInClass "#\n")
_ <- (string hashes >> char '\n') <|> (char '\n')
return $ SectionHeader (T.length hashes) (parseGtkDoc heading)
-- | Parse a list header. Note that the newline before the start of
-- the list is "eaten" by this parser, but is restored later by
-- `parseGtkDoc`.
--
-- === __Examples__
-- >>> parseOnly (parseList <* endOfInput) "\n- First item\n- Second item"
-- Right (List [ListItem (GtkDoc [Literal "First item"]) [],ListItem (GtkDoc [Literal "Second item"]) []])
--
-- >>> parseOnly (parseList <* endOfInput) "\n\n- Two line\n item\n\n- Second item,\n also two lines"
-- Right (List [ListItem (GtkDoc [Literal "Two line"]) [GtkDoc [Literal "item"]],ListItem (GtkDoc [Literal "Second item,"]) [GtkDoc [Literal "also two lines"]]])
parseList :: Parser Token
parseList = do
items <- many1 parseListItem
return $ List items
where parseListItem :: Parser ListItem
parseListItem = do
_ <- char '\n'
_ <- string "\n- " <|> string "- "
first <- takeWhile1 (/= '\n')
rest <- many' parseLine
return $ ListItem (parseGtkDoc first) (map parseGtkDoc rest)
parseLine :: Parser Text
parseLine = string "\n " >> takeWhile1 (/= '\n')