pandoc-3.2: src/Text/Pandoc/Readers/RIS.hs
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : Text.Pandoc.Readers.RIS
Copyright : Copyright (C) 2022-2024 John MacFarlane
License : GNU GPL, version 2 or above
Maintainer : John MacFarlane <jgm@berkeley.edu>
Stability : alpha
Portability : portable
Parses RIS bibliographies into a Pandoc document
with empty body and `references` and `nocite` fields
in the metadata. A wildcard `nocite` is used so that
if the document is rendered in another format, the
entire bibliography will be printed.
-}
module Text.Pandoc.Readers.RIS
( readRIS
)
where
import Text.Pandoc.Options
import Text.Pandoc.Definition
import Text.Pandoc.Parsing
import Data.Char (isAsciiUpper, isDigit, isSpace, ord, chr)
import Data.List (foldl')
import Citeproc (Reference(..), ItemId(..), Val(..), Date(..), DateParts(..),
toVariable)
import Text.Pandoc.Builder as B
import Text.Pandoc.Class (PandocMonad)
import Text.Pandoc.Citeproc.MetaValue (referenceToMetaValue)
import Text.Pandoc.Citeproc.Name (toName, NameOpts(..))
import Control.Monad.Except (throwError)
import qualified Data.Text as T
import Data.Text (Text)
import Data.Maybe (fromMaybe)
import qualified Data.Map as M
import Safe (readMay)
-- | Read RIS from an input string and return a Pandoc document.
-- The document will have only metadata, with an empty body.
-- The metadata will contain a `references` field with the
-- bibliography entries, and a `nocite` field with the wildcard `[@*]`.
readRIS :: (PandocMonad m, ToSources a)
=> ReaderOptions -> a -> m Pandoc
readRIS _opts inp = do
parsed <- readWithM risReferences () inp
case parsed of
Right refs -> do
refs' <- mapM (traverse (return . text)) refs
return $
setMeta "nocite" (cite [Citation {citationId = "*"
, citationPrefix = []
, citationSuffix = []
, citationMode = NormalCitation
, citationNoteNum = 0
, citationHash = 0}] (str "[@*]")) $
setMeta "references" (map referenceToMetaValue refs') $
B.doc mempty
Left e -> throwError e
type RISParser m = ParsecT Sources () m
risLine :: PandocMonad m => RISParser m (Text, Text)
risLine = do
key <- T.pack <$> count 2 (satisfy (\c -> isAsciiUpper c || isDigit c))
_ <- many1 spaceChar
char '-'
val <- (many1 spaceChar *> anyLine) <|> mempty <$ newline
return (key, T.strip val)
risSeparator :: PandocMonad m => RISParser m ()
risSeparator = do
try $ string "ER"
_ <- many1 spaceChar
char '-'
_ <- anyLine
optional blanklines
risRecord :: PandocMonad m => RISParser m [(Text, Text)]
risRecord = manyTill risLine risSeparator
risRecordToReference :: [(Text, Text)] -> Reference Text
risRecordToReference keys = addId $ foldr go defref keys
where
go (key, val) =
case key of
"TY" -> \ref -> ref{ referenceType =
fromMaybe "misc" (M.lookup val risTypes) }
"ID" -> \ref -> ref{ referenceId = ItemId val }
"VL" -> addVar "volume" val
"KW" -> \ref ->
ref{ referenceVariables =
M.alter (\x -> case x of
Nothing -> Just $ TextVal val
Just (TextVal kws)
-> Just (TextVal (kws <> ", " <> val))
_ -> x)
"keyword"
(referenceVariables ref) }
"PB" -> addVar "publisher" val
"PP" -> addVar "publisher-place" val
"DO" -> addVar "DOI" val
"SP" -> \ref ->
case M.lookup "page" (referenceVariables ref) of
Nothing -> addVar "page" val ref
Just (FancyVal eg) -> addVar "page" (val <> eg) ref
_ -> ref
"EP" -> \ref ->
case M.lookup "page" (referenceVariables ref) of
Nothing -> addVar "page" ("-" <> val) ref
Just (FancyVal eg) -> addVar "page" (val <> "-" <> eg) ref
_ -> ref
"AU" -> addName "author" val
"A1" -> addName "author" val
"ED" -> addName "editor" val
"A2" -> addName "editor" val
"TI" -> addVar "title" val
"T1" -> addVar "title" val
"CT" -> addVar "title" val
"BT" -> \ref ->
if referenceType ref == "book"
then addVar "title" val ref
else addVar "container-title" val ref
"JO" -> addVar "container-title" val
"JF" -> addVar "container-title" val
"T2" -> addVar "container-title" val
"ET" -> addVar "edition" val
"NV" -> addVar "number-of-volumes" val
"AB" -> addVar "abstract" val
"PY" -> addYear "issued" val
"Y1" -> addYear "issued" val
"IS" -> addVar "issue" val
"SN" -> addVar "ISSN" val
"LA" -> addVar "language" val
"UR" -> addVar "url" val
"LK" -> addVar "url" val
_ -> id -- TODO
addVar k v r = r{ referenceVariables =
M.insert (toVariable k) (FancyVal v)
(referenceVariables r) }
addName k v r =
let new = toName NameOpts{ nameOptsPrefixIsNonDroppingParticle = False
, nameOptsUseJuniorComma = False }
. B.toList . B.text $ v
f Nothing = Just (NamesVal new)
f (Just (NamesVal ns)) = Just (NamesVal (new ++ ns))
f (Just x) = Just x
in r{ referenceVariables =
M.alter f k (referenceVariables r) }
addYear k v r =
let d = DateVal $
case readMay (T.unpack v) of
Nothing ->
Date { dateParts = []
, dateCirca = False
, dateSeason = Nothing
, dateLiteral = Just v }
Just y ->
Date { dateParts = [DateParts [y]]
, dateCirca = False
, dateSeason = Nothing
, dateLiteral = Nothing }
in r{ referenceVariables = M.insert k d (referenceVariables r) }
defref = Reference{
referenceId = mempty
, referenceType = mempty
, referenceDisambiguation = Nothing
, referenceVariables = mempty }
addId rec =
if referenceId rec == mempty
then rec{ referenceId = ItemId (authors <> pubdate) }
else rec
authors = T.intercalate "_" $
[T.takeWhile (\c -> c /= ',' && not (isSpace c)) n
| (k, n) <- keys, k == "AU" || k == "A1"]
pubdate = mconcat ["_" <> d | (k, d) <- keys, k == "PY" || k == "Y1"]
risReferences :: PandocMonad m => RISParser m [Reference Text]
risReferences = do
recs <- many risRecord
spaces
eof
return $ fixDuplicateIds $ map risRecordToReference recs
fixDuplicateIds :: [Reference Text] -> [Reference Text]
fixDuplicateIds = reverse . snd . foldl' go (mempty, [])
where
go (ids_seen, refs) ref =
case M.lookup (referenceId ref) ids_seen of
Nothing -> (M.insert (referenceId ref) (ord 'a') ids_seen, ref:refs)
Just n -> (M.insert (referenceId ref) (n+1) ids_seen,
ref{ referenceId =
ItemId . (<> T.singleton (chr n)) . unItemId $
referenceId ref }
: refs)
risTypes :: M.Map Text Text
risTypes = M.fromList
[ ("ABST", "article")
, ("ADVS", "motion-picture")
, ("AGGR", "dataset")
, ("ANCIENT", "book")
, ("ART", "graphic")
, ("BILL", "bill")
, ("BLOG", "post-weblog")
, ("BOOK", "book")
, ("CASE", "legal_case")
, ("CHAP", "chapter")
, ("CHART", "graphic")
, ("CLSWK", "book")
, ("COMP", "program")
, ("CONF", "paper-conference")
, ("CPAPER", "paper-conference")
, ("CTLG", "catalog")
, ("DATA", "dataset")
, ("DBASE", "dataset")
, ("DICT", "book")
, ("EBOOK", "book")
, ("ECHAP", "chapter")
, ("EDBOOK", "book")
, ("EJOUR", "article")
, ("WEB", "webpage")
, ("ENCYC", "entry-encyclopedia")
, ("EQUA", "figure")
, ("FIGURE", "figure")
, ("GEN", "entry")
, ("GOVDOC", "report")
, ("GRANT", "report")
, ("HEAR", "report")
, ("ICOMM", "personal_communication")
, ("INPR", "article-journal")
, ("JFULL", "article-journal")
, ("JOUR", "article-journal")
, ("LEGAL", "legal_case")
, ("MANSCPT", "manuscript")
, ("MAP", "map")
, ("MGZN", "article-magazine")
, ("MPCT", "motion-picture")
, ("MULTI", "webpage")
, ("MUSIC", "musical_score")
, ("NEWS", "article-newspaper")
, ("PAMP", "pamphlet")
, ("PAT", "patent")
, ("PCOMM", "personal_communication")
, ("RPRT", "report")
, ("SER", "article")
, ("SLIDE", "graphic")
, ("SOUND", "musical_score")
, ("STAND", "report")
, ("STAT", "legislation")
, ("THES", "thesis")
, ("UNBILL", "bill")
, ("UNPB", "unpublished")
, ("VIDEO", "graphic") ]