mirror of
https://github.com/jgm/pandoc.git
synced 2026-08-28 17:20:47 +08:00
d226a35c0a
Reasons: - Performance: HsYAML is around 20 times slower in parsing large YAML bibliographies (#6084). - An issue was submitted to HsYAML, but it hasn't gotten any attention. HsYAML seems borderline unmaintained; it hasn't had a commit in over a year. - Unfortunately this goes back on our attempts to free ourselves from C dependencies (#4535). But I don't see a better alternative until a better pure Haskell parser is available. Closes #6084. Notes: - We've removed the FromYAML instances for all types that had them, since this is a HsYAML-specific typeclass [API change]. (The yaml package just uses From/ToJSON.) - Unlike HsYAML (in the configuration we were using), yaml parses 'Y', 'N', 'Yes', 'No', 'On', 'Off' as boolean values. Users may need to quote these when they are meant to be interpreted as strings. Similarly, 'null' is parsed as a YAML null value (and will be treated as an empty string by pandoc rather than the string 'null'). Quoting it will force it to be interpreted as a string. - Some tests had to be adjusted accordingly. - Pandoc now behaves better when the YAML metadata contains escaping errors: instead of just falling back on treating the section as a table, it raises a YAML parsing error.
149 lines
5.6 KiB
Haskell
149 lines
5.6 KiB
Haskell
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{- |
|
|
Module : Text.Pandoc.Readers.Metadata
|
|
Copyright : Copyright (C) 2006-2021 John MacFarlane
|
|
License : GNU GPL, version 2 or above
|
|
|
|
Maintainer : John MacFarlane <jgm@berkeley.edu>
|
|
Stability : alpha
|
|
Portability : portable
|
|
|
|
Parse YAML/JSON metadata to 'Pandoc' 'Meta'.
|
|
-}
|
|
module Text.Pandoc.Readers.Metadata (
|
|
yamlBsToMeta,
|
|
yamlBsToRefs,
|
|
yamlMetaBlock,
|
|
yamlMap ) where
|
|
|
|
|
|
import Control.Monad.Except (throwError)
|
|
import qualified Data.ByteString as B
|
|
import qualified Data.Map as M
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import qualified Data.Yaml as Yaml
|
|
import Data.Aeson (Value(..), Object, Result(..), fromJSON, (.:?), withObject)
|
|
import Data.Aeson.Types (parse)
|
|
import Text.Pandoc.Shared (tshow)
|
|
import Text.Pandoc.Class.PandocMonad (PandocMonad (..))
|
|
import Text.Pandoc.Definition hiding (Null)
|
|
import Text.Pandoc.Error
|
|
import Text.Pandoc.Parsing hiding (tableWith, parse)
|
|
|
|
|
|
import qualified Text.Pandoc.UTF8 as UTF8
|
|
|
|
yamlBsToMeta :: (PandocMonad m, HasLastStrPosition st)
|
|
=> ParserT Sources st m (Future st MetaValue)
|
|
-> B.ByteString
|
|
-> ParserT Sources st m (Future st Meta)
|
|
yamlBsToMeta pMetaValue bstr = do
|
|
case Yaml.decodeAllEither' bstr of
|
|
Right (Object o:_) -> fmap Meta <$> yamlMap pMetaValue o
|
|
Right [Null] -> return . return $ mempty
|
|
Right _ -> Prelude.fail "expected YAML object"
|
|
Left err' -> do
|
|
throwError $ PandocParseError
|
|
$ T.pack $ Yaml.prettyPrintParseException err'
|
|
|
|
-- Returns filtered list of references.
|
|
yamlBsToRefs :: (PandocMonad m, HasLastStrPosition st)
|
|
=> ParserT Sources st m (Future st MetaValue)
|
|
-> (Text -> Bool) -- ^ Filter for id
|
|
-> B.ByteString
|
|
-> ParserT Sources st m (Future st [MetaValue])
|
|
yamlBsToRefs pMetaValue idpred bstr =
|
|
case Yaml.decodeEither' bstr of
|
|
Right (Object m) -> do
|
|
let isSelected (String t) = idpred t
|
|
isSelected _ = False
|
|
let hasSelectedId (Object o) =
|
|
case parse (withObject "ref" (.:? "id")) (Object o) of
|
|
Success (Just id') -> isSelected id'
|
|
_ -> False
|
|
hasSelectedId _ = False
|
|
case parse (withObject "metadata" (.:? "references")) (Object m) of
|
|
Success (Just refs) -> sequence <$>
|
|
mapM (yamlToMetaValue pMetaValue) (filter hasSelectedId refs)
|
|
_ -> return $ return []
|
|
Right _ -> return . return $ []
|
|
Left err' -> do
|
|
throwError $ PandocParseError
|
|
$ T.pack $ Yaml.prettyPrintParseException err'
|
|
|
|
normalizeMetaValue :: (PandocMonad m, HasLastStrPosition st)
|
|
=> ParserT Sources st m (Future st MetaValue)
|
|
-> Text
|
|
-> ParserT Sources st m (Future st MetaValue)
|
|
normalizeMetaValue pMetaValue x =
|
|
-- Note: a standard quoted or unquoted YAML value will
|
|
-- not end in a newline, but a "block" set off with
|
|
-- `|` or `>` will.
|
|
if "\n" `T.isSuffixOf` T.dropWhileEnd isSpaceChar x -- see #6823
|
|
then parseFromString' pMetaValue (x <> "\n")
|
|
else parseFromString' asInlines x
|
|
where asInlines = fmap b2i <$> pMetaValue
|
|
b2i (MetaBlocks [Plain ils]) = MetaInlines ils
|
|
b2i (MetaBlocks [Para ils]) = MetaInlines ils
|
|
b2i bs = bs
|
|
isSpaceChar ' ' = True
|
|
isSpaceChar '\t' = True
|
|
isSpaceChar _ = False
|
|
|
|
yamlToMetaValue :: (PandocMonad m, HasLastStrPosition st)
|
|
=> ParserT Sources st m (Future st MetaValue)
|
|
-> Value
|
|
-> ParserT Sources st m (Future st MetaValue)
|
|
yamlToMetaValue pMetaValue v =
|
|
case v of
|
|
String t -> normalizeMetaValue pMetaValue t
|
|
Bool b -> return $ return $ MetaBool b
|
|
Number d -> normalizeMetaValue pMetaValue $
|
|
case fromJSON v of
|
|
Success (x :: Int) -> tshow x
|
|
_ -> tshow d
|
|
Null -> return $ return $ MetaString ""
|
|
Array{} -> do
|
|
case fromJSON v of
|
|
Error err' -> throwError $ PandocParseError $ T.pack err'
|
|
Success xs -> fmap MetaList . sequence <$>
|
|
mapM (yamlToMetaValue pMetaValue) xs
|
|
Object o -> fmap MetaMap <$> yamlMap pMetaValue o
|
|
|
|
yamlMap :: (PandocMonad m, HasLastStrPosition st)
|
|
=> ParserT Sources st m (Future st MetaValue)
|
|
-> Object
|
|
-> ParserT Sources st m (Future st (M.Map Text MetaValue))
|
|
yamlMap pMetaValue o = do
|
|
case fromJSON (Object o) of
|
|
Error err' -> throwError $ PandocParseError $ T.pack err'
|
|
Success (m' :: M.Map Text Value) -> do
|
|
let kvs = filter (not . ignorable . fst) $ M.toList m'
|
|
fmap M.fromList . sequence <$> mapM toMeta kvs
|
|
where
|
|
ignorable t = "_" `T.isSuffixOf` t
|
|
toMeta (k, v) = do
|
|
fv <- yamlToMetaValue pMetaValue v
|
|
return $ do
|
|
v' <- fv
|
|
return (k, v')
|
|
|
|
-- | Parse a YAML metadata block using the supplied 'MetaValue' parser.
|
|
yamlMetaBlock :: (HasLastStrPosition st, PandocMonad m)
|
|
=> ParserT Sources st m (Future st MetaValue)
|
|
-> ParserT Sources st m (Future st Meta)
|
|
yamlMetaBlock parser = try $ do
|
|
string "---"
|
|
blankline
|
|
notFollowedBy blankline -- if --- is followed by a blank it's an HRULE
|
|
rawYamlLines <- manyTill anyLine stopLine
|
|
-- by including --- and ..., we allow yaml blocks with just comments:
|
|
let rawYaml = T.unlines ("---" : (rawYamlLines ++ ["..."]))
|
|
optional blanklines
|
|
yamlBsToMeta parser $ UTF8.fromText rawYaml
|
|
|
|
stopLine :: Monad m => ParserT Sources st m ()
|
|
stopLine = try $ (string "---" <|> string "...") >> blankline >> return ()
|