packages feed

rdf4h-5.0.1: src/Text/RDF/RDF4H/NTriplesParser.hs

{-# LANGUAGE CPP #-}

-- | A parser for RDF in N-Triples format
--  <http://www.w3.org/TR/rdf-testcases/#ntriples>.
module Text.RDF.RDF4H.NTriplesParser
  ( NTriplesParser (NTriplesParser),
    NTriplesParserCustom (NTriplesParserCustom),
    ParseFailure,
    nt_echar,
    nt_uchar,
    nt_langtag,
    string_literal_quote,
    nt_string_literal_quote,
    nt_pn_chars_base,
    nt_comment,
    readFile,
  )
where

#if MIN_VERSION_base(4,9,0)
#if !MIN_VERSION_base(4,11,0)
import Data.Semigroup ((<>))
#else
#endif
#else
#endif

import Control.Applicative
import Control.Monad (void)
import Data.Attoparsec.ByteString (IResult (..), parse)
import Data.Char (isAlphaNum, isAsciiLower, isAsciiUpper, isDigit, isLetter)
import Data.RDF.IRI
import Data.RDF.Types hiding (empty)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.IO as T
import System.IO (IOMode (..), hSetEncoding, hSetNewlineMode, noNewlineTranslation, utf8, withFile)
import Text.Parsec (ParseError, runParser)
import Text.Parser.Char
import Text.Parser.Combinators
import Text.Parser.LookAhead
import Text.RDF.RDF4H.ParserUtils
import Prelude hiding (readFile)

-- | NTriplesParser is an 'RdfParser' implementation for parsing RDF in the
--  NTriples format. It requires no configuration options. To use this parser,
--  pass an 'NTriplesParser' value as the first argument to any of the
--  'parseString', 'parseFile', or 'parseURL' methods of the 'RdfParser' type
--  class.
data NTriplesParser = NTriplesParser

newtype NTriplesParserCustom = NTriplesParserCustom Parser

-- | 'NTriplesParser' is an instance of 'RdfParser' using parsec based parsers.
instance RdfParser NTriplesParser where
  parseString _ = parseStringParsec
  parseFile _ = parseFileParsec
  parseURL _ = parseURLParsec

-- | 'NTriplesParser' is an instance of 'RdfParser'.
instance RdfParser NTriplesParserCustom where
  parseString (NTriplesParserCustom Parsec) = parseStringParsec
  parseString (NTriplesParserCustom Attoparsec) = parseStringAttoparsec
  parseFile (NTriplesParserCustom Parsec) = parseFileParsec
  parseFile (NTriplesParserCustom Attoparsec) = parseFileAttoparsec
  parseURL (NTriplesParserCustom Parsec) = parseURLParsec
  parseURL (NTriplesParserCustom Attoparsec) = parseURLAttoparsec

-- We define or redefine all here using same names as the spec, but with an
-- 'nt_' prefix in order to avoid name clashes (e.g., ntripleDoc becomes
-- nt_ntripleDoc).

-- | nt_ntripleDoc is simply zero or more lines.
--  grammar [1] ntriplesDoc ::= triple? (EOL triple)* EOL?
nt_ntripleDoc :: (CharParsing m, LookAheadParsing m, Monad m) => m [Triple]
nt_ntripleDoc = many sep *> sepEndBy (try nt_triple) (many sep) <* eof
  where
    sep = many nt_space *> (try nt_comment <|> try nt_eoln) *> many nt_space

-- A triple consists of whitespace-delimited subject, predicate, and object,
-- followed by optional whitespace and a period, and possibly more
-- whitespace.
--
-- NTriples W3C test "minimal_whitespace" proposes no space:
--
-- "tests absense of whitespace between subject, predicate, object and
-- end-of-statement"
--
-- `optional` lets this nt_triple parser succeed even if there is not
-- a space or tab character between resources or the object and the '.'.
-- Grammar [2] triple ::= subject predicate object '.'
nt_triple :: (CharParsing m, LookAheadParsing m, Monad m) => m Triple
nt_triple =
  Triple
    <$> (nt_subject <* optional (skipSome nt_space))
    <*> (nt_predicate <* optional (skipSome nt_space))
    <*> (nt_object <* optional (skipSome nt_space) <* char '.' <* many nt_space)

-- Grammar [6] literal ::= STRING_LITERAL_QUOTE ('^^' IRIREF | LANGTAG)?
nt_literal :: (CharParsing m, Monad m) => m LValue
nt_literal = do
  str <- nt_string_literal_quote
  option (plainL str) (langTag str <|> typeIRI str)
  where
    langTag str = plainLL str <$> try nt_langtag
    typeIRI str = typedL str <$> try (count 2 (char '^') *> nt_iriref)

-- Grammar [9] STRING_LITERAL_QUOTE ::= '"' ([^#x22#x5C#xA#xD] | ECHAR | UCHAR)* '"'
nt_string_literal_quote :: (CharParsing m, Monad m) => m T.Text
nt_string_literal_quote = string_literal_quote '"'

string_literal_quote :: (CharParsing m, Monad m) => Char -> m T.Text
string_literal_quote d = between (char d) (char d) string_literal
  where
    string_literal = T.pack <$> many (try validLiteralChar)
    validLiteralChar =
      noneOf [d, '\x5C', '\xA', '\xD']
        <|> nt_echar
        <|> nt_uchar

-- Grammar [144s] LANGTAG ::= '@' [a-zA-Z]+ ('-' [a-zA-Z0-9]+)*
nt_langtag :: (CharParsing m, Monad m) => m T.Text
nt_langtag = do
  ss <- char '@' *> some (satisfy isLetter)
  rest <- concat <$> many (char '-' *> some (satisfy isAlphaNum) >>= \lang_str -> pure ('-' : lang_str))
  pure (T.pack (ss <> rest))

-- [8] IRIREF
nt_iriref :: (CharParsing m, Monad m) => m T.Text
nt_iriref = between (char '<') (char '>') $ do
  raw_iri <- iriFragment
  either (const empty) pure (validateIRI raw_iri) <?> "Only absolute IRIs allowed in NTriples format, which this isn't: " <> show raw_iri

-- [153s] ECHAR
nt_echar :: (CharParsing m, Monad m) => m Char
nt_echar = try $ do
  c2 <- char '\\' *> anyChar
  case c2 of
    't' -> pure '\t'
    'b' -> pure '\b'
    'n' -> pure '\n'
    'r' -> pure '\r'
    'f' -> pure '\f'
    '"' -> pure '\"'
    '\'' -> pure '\''
    '\\' -> pure '\\'
    _ -> empty

-- [10] UCHAR ::= '\u' HEX HEX HEX HEX | '\U' HEX HEX HEX HEX HEX HEX HEX HEX
nt_uchar :: (CharParsing m, Monad m) => m Char
nt_uchar = uchar

-- A subject is either a URI reference for a resource or a node id for a
-- blank node.
nt_subject :: (CharParsing m, LookAheadParsing m, Monad m) => m Node
nt_subject =
  unode <$> try nt_iriref
    <|> bnodeUnsafe <$> nt_blank_node_label

-- A predicate may only be a URI reference to a resource.
nt_predicate :: (CharParsing m, Monad m) => m Node
nt_predicate = unode <$> nt_iriref

-- An object may be either a resource (represented by a URI reference),
-- a blank node (represented by a node id), or an object literal.
nt_object :: (CharParsing m, LookAheadParsing m, Monad m) => m Node
nt_object =
  unode <$> try nt_iriref
    <|> bnodeUnsafe <$> try nt_blank_node_label
    <|> LNode <$> nt_literal

-- [141s] BLANK_NODE_LABEL ::= '_:' (PN_CHARS_U | [0-9]) ((PN_CHARS | '.')* PN_CHARS)?
nt_blank_node_label :: (CharParsing m, LookAheadParsing m, Monad m) => m T.Text
nt_blank_node_label = do
  void (string "_:")
  firstChar <- nt_pn_chars_u <|> satisfy isDigit
  otherChars <-
    option "" $
      try $
        many (nt_pn_chars <|> try (char '.' <* lookAhead (try nt_pn_chars)))
  pure $ T.pack (firstChar : otherChars)

-- [157s] PN_CHARS_BASE ::= [A-Z] | [a-z] | [#x00C0-#x00D6] | [#x00D8-#x00F6] | [#x00F8-#x02FF] | [#x0370-#x037D] | [#x037F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
nt_pn_chars_base :: CharParsing m => m Char
nt_pn_chars_base = try $ satisfy isBaseChar
  where
    isBaseChar c =
      (isAsciiUpper c)
        || (isAsciiLower c)
        || (c >= '\x00C0' && c <= '\x00D6')
        || (c >= '\x00D8' && c <= '\x00F6')
        || (c >= '\x00F8' && c <= '\x02FF')
        || (c >= '\x0370' && c <= '\x037D')
        || (c >= '\x037F' && c <= '\x1FFF')
        || (c >= '\x200C' && c <= '\x200D')
        || (c >= '\x2070' && c <= '\x218F')
        || (c >= '\x2C00' && c <= '\x2FEF')
        || (c >= '\x3001' && c <= '\xD7FF')
        || (c >= '\xF900' && c <= '\xFDCF')
        || (c >= '\xFDF0' && c <= '\xFFFD')
        || (c >= '\x10000' && c <= '\xEFFFF')

-- [158s] PN_CHARS_U
nt_pn_chars_u :: CharParsing m => m Char
nt_pn_chars_u = nt_pn_chars_base <|> try (char '_') <|> try (char ':')

-- [160s] PN_CHARS
nt_pn_chars :: CharParsing m => m Char
nt_pn_chars =
  nt_pn_chars_u
    <|> try (char '-')
    <|> try (char '\x00B7')
    <|> try (satisfy f)
  where
    f c =
      isDigit c
        || (c >= '\x0300' && c <= '\x036F')
        || (c >= '\x203F' && c <= '\x2040')

-- End-of-line consists of either lf or crlf.
-- We also test for eof and consider that to match as well.
nt_eoln :: CharParsing m => m ()
nt_eoln = try (void (string "\r\n")) <|> void (char '\n')

-- Whitespace is either a space or a tabulation.
-- The built-in space combinator must be avoided here, because it includes newline.
nt_space :: CharParsing m => m ()
nt_space = void (try (char ' ') <|> try (char '\t'))

nt_comment :: CharParsing m => m ()
nt_comment = void (char '#' *> manyTill anyChar (try nt_eoln))

---------------------------------
-- parsec based parsers

parseStringParsec :: (Rdf a) => T.Text -> Either ParseFailure (RDF a)
parseStringParsec bs = handleParsec mkRdf (runParser nt_ntripleDoc () "" bs)

parseFileParsec :: (Rdf a) => String -> IO (Either ParseFailure (RDF a))
parseFileParsec path =
  handleParsec mkRdf . runParser nt_ntripleDoc () path
    <$> readFile path

readFile :: FilePath -> IO T.Text
readFile fpath = withFile fpath ReadMode $ \h -> do
  hSetNewlineMode h noNewlineTranslation
  hSetEncoding h utf8
  T.hGetContents h

parseURLParsec :: (Rdf a) => String -> IO (Either ParseFailure (RDF a))
parseURLParsec = parseFromURL parseStringParsec

handleParsec ::
  (Triples -> Maybe BaseUrl -> PrefixMappings -> RDF a) ->
  Either ParseError [Triple] ->
  Either ParseFailure (RDF a)
handleParsec _mkRdf result = case result of
  Left err -> Left $ ParseFailure $ "Parse failure: \n" <> show err
  Right ts -> Right $ _mkRdf ts Nothing (PrefixMappings mempty)

---------------------------------

---------------------------------
-- attoparsec based parsers

parseFileAttoparsec :: (Rdf a) => String -> IO (Either ParseFailure (RDF a))
parseFileAttoparsec path = handleAttoparsec <$> readFile path

parseURLAttoparsec :: (Rdf a) => String -> IO (Either ParseFailure (RDF a))
parseURLAttoparsec = parseFromURL handleAttoparsec

parseStringAttoparsec :: (Rdf a) => T.Text -> Either ParseFailure (RDF a)
parseStringAttoparsec = handleAttoparsec

handleAttoparsec :: (Rdf a) => T.Text -> Either ParseFailure (RDF a)
handleAttoparsec bs = handleResult $ parse nt_ntripleDoc (T.encodeUtf8 bs)
  where
    handleResult res = case res of
      Fail _i _contexts err -> Left $ ParseFailure $ "Parse failure: \n" <> show err
      -- error $
      -- "\nnot consumed: " <> show i
      -- <> "\ncontexts: " <> show contexts
      -- <> "\nerror: " <> show err
      Partial f -> handleResult (f (T.encodeUtf8 mempty))
      Done _ ts -> Right $ mkRdf ts Nothing (PrefixMappings mempty)

---------------------------------