packages feed

rdf4h-5.2.2: src/Text/RDF/RDF4H/XmlParser/Identifiers.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}

module Text.RDF.RDF4H.XmlParser.Identifiers
  ( -- rdf:ID validation
    checkRdfId,
    -- Qualified names
    resolveQName,
    resolveQName',
    parseQName,
  )
where

#if !MIN_VERSION_base(4,13,0)
import           Data.Functor ((<$))
#else
#endif
import Control.Applicative (Alternative (..), liftA2)
import Data.Attoparsec.Text (Parser, (<?>))
import qualified Data.Attoparsec.Text as P
import qualified Data.Map as Map
import Data.Text (Text)
import qualified Data.Text as T
#if MIN_VERSION_base(4,9,0)
#if !MIN_VERSION_base(4,11,0)
import Data.Semigroup ((<>))
#else
#endif
#else
#endif
import Data.Char (isAsciiLower, isAsciiUpper, isDigit)
import Data.RDF.Namespace

--------------------------------------------------------------------------------
-- rdf:ID

-- | Validate the value of @rdf:ID@.
--
--  See: https://www.w3.org/TR/rdf-syntax-grammar/#rdf-id
checkRdfId ::
  -- | Value of a @rdf:ID@ attribute to validate.
  Text ->
  Either String Text
checkRdfId t = t <$ parseId t

parseId :: Text -> Either String Text
parseId = P.parseOnly $ pNCName <* (P.endOfInput <?> "Unexpected characters at the end")

--------------------------------------------------------------------------------
-- Qualified names

-- | Parse and resolve a qualified name.
--
--  See: https://www.w3.org/TR/xml-names/#ns-qualnames
resolveQName ::
  -- | Namespace mapping to resolve q qualified name.
  PrefixMappings ->
  -- | Raw qualified name to process.
  Text ->
  Either String Text
resolveQName pm qn = parseQName qn >>= resolveQName' pm

-- | Resolve a qualified name.
resolveQName' ::
  -- | Namespace mapping to resolve q qualified name.
  PrefixMappings ->
  -- | (namespace, local name)
  (Maybe Text, Text) ->
  Either String Text
resolveQName' (PrefixMappings pm) (Nothing, name) =
  case Map.lookup mempty pm of
    Nothing -> Left $ mconcat ["Cannot resolve QName \"", T.unpack name, "\": no default namespace defined."]
    Just iri -> Right $ iri <> name
resolveQName' (PrefixMappings pm) (Just prefix, name) =
  case Map.lookup prefix pm of
    Nothing -> Left $ mconcat ["Cannot resolve QName: prefix \"", T.unpack prefix, "\" not defined"]
    Just iri -> Right $ iri <> name

-- | Parse a qualified name.
--
--  See: https://www.w3.org/TR/xml-names/#ns-qualnames
parseQName :: Text -> Either String (Maybe Text, Text)
parseQName = P.parseOnly $ pQName <* (P.endOfInput <?> "Unexpected characters at the end of a QName")

-- https://www.w3.org/TR/xml-names/#ns-qualnames
-- https://www.w3.org/TR/xml-names/#NT-QName
pQName :: Parser (Maybe Text, Text)
pQName = pPrefixedName <|> pUnprefixedNamed
  where
    pUnprefixedNamed = (empty,) <$> pLocalPart

-- https://www.w3.org/TR/xml-names/#NT-PrefixedName
pPrefixedName :: Parser (Maybe Text, Text)
pPrefixedName = do
  prefix <- pLocalPart <* P.char ':'
  localPart <- pLocalPart
  pure (Just prefix, localPart)

-- https://www.w3.org/TR/xml-names/#NT-LocalPart
pLocalPart :: Parser Text
pLocalPart = pNCName

-- http://www.w3.org/TR/REC-xml-names/#NT-NCName
pNCName :: Parser Text
pNCName = liftA2 T.cons pNameStartChar pNameRest
  where
    pNameStartChar = P.satisfy isValidFirstCharId
    pNameRest = P.takeWhile isValidRestCharId
    isValidFirstCharId c =
      isAsciiUpper c
        || c == '_'
        || isAsciiLower c
        || ('\xC0' <= c && c <= '\xD6')
        || ('\xD8' <= c && c <= '\xF6')
        || ('\xF8' <= c && c <= '\x2FF')
        || ('\x370' <= c && c <= '\x37D')
        || ('\x37F' <= c && c <= '\x1FFF')
        || ('\x200C' <= c && c <= '\x200D')
        || ('\x2070' <= c && c <= '\x218F')
        || ('\x2C00' <= c && c <= '\x2FEF')
        || ('\x3001' <= c && c <= '\xD7FF')
        || ('\xF900' <= c && c <= '\xFDCF')
        || ('\xFDF0' <= c && c <= '\xFFFD')
        || ('\x10000' <= c && c <= '\xEFFFF')
    isValidRestCharId c =
      isValidFirstCharId c
        || c == '-'
        || c == '.'
        || isDigit c
        || ('\x0300' <= c && c <= '\x036F')
        || ('\x203F' <= c && c <= '\x2040')