packages feed

citeproc-0.11: src/Citeproc/Style.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TupleSections #-}
module Citeproc.Style
  ( parseStyle
  , mergeLocales
  )
where
import Citeproc.Types
import Citeproc.Locale
import Citeproc.Element
import Data.Text (Text)
import Control.Monad (foldM)
import Control.Applicative ((<|>))
import qualified Text.XML as X
import qualified Data.Text as T
import qualified Data.Map as M
import Data.Maybe (fromMaybe, isNothing)
import Data.Default (def)
import qualified Data.Text.Lazy as TL

-- | Merge the locale specified by the first parameter, if any,
-- with the default locale of the style and locale definitions
-- in the style.  The locale specified by the first parameter
-- overrides the style's defaults when there is a conflict.
mergeLocales :: Maybe Lang -> Style a -> Locale
mergeLocales mblang style =
  mconcat stylelocales <> deflocale -- left-biased union
 where
  getUSLocale = case getLocale (Lang "en" Nothing (Just"US") [] [] []) of
                  Right l -> l
                  Left _  -> mempty
  lang = fromMaybe (Lang "en" Nothing (Just"US") [] [] []) $
              mblang <|> styleDefaultLocale (styleOptions style)
  deflocale = case getLocale lang of
                 Right l -> l
                 Left _  -> getUSLocale
  primlang = getPrimaryDialect lang
  stylelocales =  -- exact match to lang gets precedence
                 [l | l <- styleLocales style
                    , localeLanguage l == Just lang] ++
                 -- then match to primary dialect, if different
                 [l | primlang /= Just lang
                    , l <- styleLocales style
                    , localeLanguage l == primlang] ++
                 -- then match to the two letter language
                 [l | l <- styleLocales style
                    , (langRegion <$> localeLanguage l) == Just Nothing
                    , (langLanguage <$> localeLanguage l) ==
                      Just (langLanguage lang)] ++
                 -- then locale with no lang
                 [l | l <- styleLocales style
                    , isNothing (localeLanguage l)]



-- | Parse an XML stylesheet into a 'Style'.  The first parameter
-- is a function that retrieves the text of the independent parent
-- of a dependent style, given a URL.  (This might make an HTTP
-- request or retrieve the style locally.)  If you aren't using
-- dependent styles, you may use `(\_ -> return mempty)`.
parseStyle :: Monad m
           => (Text -> m Text) -- ^ Function that takes a URL and retrieves
                               -- text of independent parent
           -> Text             -- ^ Contents of XML stylesheet
           -> m (Either CiteprocError (Style a))
parseStyle getIndependentParent t =
  -- first, see if it's a dependent or independent style
  case X.parseText def (TL.fromStrict t) of
    Left e  -> return $ Left $ CiteprocXMLError (T.pack (show e))
    Right n -> do
      let attr = getAttributes $ X.documentRoot n
      let defaultLocale =
            case lookupAttribute "default-locale" attr of
              Nothing  -> Nothing
              Just l   -> either (const Nothing) Just $ parseLang l
      let links = concatMap (getChildren "link") $ getChildren "info"
                    (X.documentRoot n)
      case [getAttributes l
              | l <- links
              , lookupAttribute "rel" (getAttributes l) ==
                  Just "independent-parent" ] of
        [] -> return $
               runElementParser $ pStyle defaultLocale $ X.documentRoot n
        (lattr:_) ->
          case lookupAttribute "href" lattr of
            Nothing -> return $ Left $ CiteprocXMLError
                          "No href attribute on link to parent style"
            Just url -> do -- get parent style
              parentTxt <- getIndependentParent url
              case X.parseText def (TL.fromStrict parentTxt) of
                Left e -> return $ Left $ CiteprocXMLError (T.pack (show e))
                Right n' -> return $
                 runElementParser $ pStyle defaultLocale $ X.documentRoot n'

pStyle :: Maybe Lang -> X.Element -> ElementParser (Style a)
pStyle defaultLocale node = do
  let attr = getAttributes node
  let nameformat = getInheritableNameFormat attr
  macros <- M.fromList <$> mapM pMacro (getChildren "macro" node)
  (cattr, citations)
      <- case getChildren "citation" node of
                 [n] -> (getAttributes n,) <$> pLayout n
                 []  -> parseFailure "No citation element present"
                 _   -> parseFailure "More than one citation element present"
  (battr, bibliography) <- case getChildren "bibliography" node of
                    [n] -> (\z -> (getAttributes n, Just z))
                              <$> pLayout n
                    []  -> return (mempty, Nothing)
                    _   -> parseFailure
                             "More than one bibliography element present"

  let disambiguateGivenNameRule =
        case lookupAttribute "givenname-disambiguation-rule" cattr of
          Just "all-names" -> AllNames
          Just "all-names-with-initials" -> AllNamesWithInitials
          Just "primary-name" -> PrimaryName
          Just "primary-name-with-initials" -> PrimaryNameWithInitials
          _ -> ByCite

  let disambigStrategy =
        DisambiguationStrategy
        { disambiguateAddNames =
            lookupAttribute "disambiguate-add-names" cattr == Just "true"
        , disambiguateAddGivenNames =
            case lookupAttribute "disambiguate-add-givenname" cattr of
              Just "true" -> Just disambiguateGivenNameRule
              _           -> Nothing
        , disambiguateAddYearSuffix =
           lookupAttribute "disambiguate-add-year-suffix" cattr ==
             Just "true"
        }

  let hasYearSuffixVariable (Element e f) =
        case e of
          EText (TextVariable _ "year-suffix") -> True
          EText (TextMacro macroname)
            | Just es' <- M.lookup macroname macros ->
                 any hasYearSuffixVariable es'
          EGroup _ es -> any hasYearSuffixVariable es
          EChoose [] -> False
          EChoose ((_,_,es):conds) -> any hasYearSuffixVariable es ||
              hasYearSuffixVariable (Element (EChoose conds) f)
          _ -> False
  let usesYearSuffixVariable =
        any hasYearSuffixVariable $
          layoutElements citations ++ maybe [] layoutElements bibliography

  let sOpts = StyleOptions
               { styleIsNoteStyle =
                   case lookupAttribute "class" attr of
                     Just "note" -> True
                     Nothing     -> True
                     _           -> False
               , styleDefaultLocale = defaultLocale
               , styleDemoteNonDroppingParticle =
                   case lookupAttribute "demote-non-dropping-particle" attr of
                     Just "never"     -> DemoteNever
                     Just "sort-only" -> DemoteSortOnly
                     _                -> DemoteDisplayAndSort
               , styleInitializeWithHyphen =
                 maybe True (== "true") $
                   lookupAttribute "initialize-with-hyphen" attr
               , stylePageRangeFormat =
                   case lookupAttribute "page-range-format" attr of
                     Just "chicago"     -> Just PageRangeChicago15
                     -- chicago is an alias for chicago-15, but this
                     -- will change to chicago-16 in v1.1
                     Just "chicago-15"  -> Just PageRangeChicago15
                     Just "chicago-16"  -> Just PageRangeChicago16
                     Just "expanded"    -> Just PageRangeExpanded
                     Just "minimal"     -> Just PageRangeMinimal
                     Just "minimal-two" -> Just PageRangeMinimalTwo
                     _                  -> Nothing
               , stylePageRangeDelimiter =
                   lookupAttribute "page-range-delimiter" attr
               , styleDisambiguation = disambigStrategy
               , styleNearNoteDistance =
                   lookupAttribute "near-note-distance" attr >>= readAsInt
               , styleCiteGroupDelimiter =
                   lookupAttribute "cite-group-delimiter" cattr <|>
                   (", " <$ lookupAttribute "collapse" cattr)
               , styleLineSpacing =
                   lookupAttribute "line-spacing" battr >>= readAsInt
               , styleEntrySpacing =
                   lookupAttribute "entry-spacing" battr >>= readAsInt
               , styleHangingIndent =
                   lookupAttribute "hanging-indent" battr == Just "true"
               , styleSecondFieldAlign =
                   case lookupAttribute "second-field-align" battr of
                     Just "flush" -> Just SecondFieldAlignFlush
                     Just "margin" -> Just SecondFieldAlignMargin
                     _ -> Nothing
               , styleSubsequentAuthorSubstitute =
                   case lookupAttribute "subsequent-author-substitute"
                        battr of
                     Nothing -> Nothing
                     Just t  -> Just $
                       SubsequentAuthorSubstitute t
                       $ case lookupAttribute
                           "subsequent-author-substitute-rule" battr of
                             Just "complete-each" -> CompleteEach
                             Just "partial-each" -> PartialEach
                             Just "partial-first" -> PartialFirst
                             _  -> CompleteAll
               , styleUsesYearSuffixVariable = usesYearSuffixVariable
               , styleNameFormat = nameformat -- TODO
               }
  locales <- mapM pLocale (getChildren "locale" node)
  let cslVersion = case lookupAttribute "version" attr of
                     Nothing -> (0,0,0)
                     Just t  ->
                       case map readAsInt (T.splitOn "." t) of
                         (Just x : Just y : Just z :_) -> (x,y,z)
                         (Just x : Just y : _)         -> (x,y,0)
                         (Just x : _)                  -> (x,0,0)
                         _                             -> (0,0,0)
  return $ Style
           { styleCslVersion     = cslVersion
           , styleOptions        = sOpts
           , styleCitation       = citations
           , styleBibliography   = bibliography
           , styleLocales        = locales
           , styleAbbreviations  = Nothing
           , styleMacros         = macros
           }

pElement :: X.Element -> ElementParser (Element a)
pElement node =
  case X.nameLocalName (X.elementName node) of
    "date"   -> pDate node
    "text"   -> pText node
    "group"  -> pGroup node
    "choose" -> pChoose node
    "number" -> pNumber node
    "label"  -> pLabel node
    "names"  -> pNames node
    name     -> parseFailure $ "unknown element " <> show name

pChoose :: X.Element -> ElementParser (Element a)
pChoose node = do
  ifNodes <- mapM parseIf $ getChildren "if" node
  elseIfNodes <- mapM parseIf $ getChildren "else-if" node
  elseNodes <- mapM parseIf $ getChildren "else" node
  let parts = ifNodes ++ elseIfNodes ++ elseNodes
  return $ Element (EChoose parts) mempty

parseIf :: X.Element -> ElementParser (Match, [Condition], [Element a])
parseIf node = do
  let attr = getAttributes node
  let match = case lookupAttribute "match" attr of
                Just "any"   -> MatchAny
                Just "none"  -> MatchNone
                _            -> MatchAll
  let conditions =
        (case lookupAttribute "disambiguate" attr of
           Just "true" -> (WouldDisambiguate :)
           _           -> id) .
        (case lookupAttribute "is-numeric" attr of
           Just t  -> \xs -> foldr ((:) . IsNumeric) xs (splitVars t)
           _       -> id) .
        (case lookupAttribute "is-uncertain-date" attr of
           Just t  -> \xs -> foldr ((:) . IsUncertainDate) xs (splitVars t)
           _       -> id) .
        (case lookupAttribute "locator" attr of
           Just t  -> \xs -> foldr ((:) . HasLocatorType) xs (splitVars t)
           _       -> id) .
        (case lookupAttribute "position" attr of
           Just t  -> \xs ->
             foldr (\case
                       "first"      -> (HasPosition FirstPosition :)
                       "ibid"       -> (HasPosition Ibid :)
                       "ibid-with-locator"
                                    -> (HasPosition IbidWithLocator :)
                       "subsequent" -> (HasPosition Subsequent :)
                       "near-note"  -> (HasPosition NearNote :)
                       _            -> id)
             xs (splitVars t)
           _       -> id) .
        (case lookupAttribute "type" attr of
           Just t  -> (HasType (T.words $ T.strip t) :)
           _       -> id) .
        (case lookupAttribute "variable" attr of
           Just t  -> \xs -> foldr ((:) . HasVariable) xs (splitVars t)
           _       -> id) $ []
  elts <- mapM pElement $ allChildren node
  return (match, conditions, elts)

pNumber :: X.Element -> ElementParser (Element a)
pNumber node = do
  let attr = getAttributes node
  let formatting = getFormatting attr
  let variable = lookupAttribute "variable" attr
  let numform = case lookupAttribute "form" attr of
                  Just "ordinal"      -> NumberOrdinal
                  Just "long-ordinal" -> NumberLongOrdinal
                  Just "roman"        -> NumberRoman
                  _                   -> NumberNumeric
  case variable of
    Nothing  -> parseFailure "number element without required variable attribute"
    Just var -> return $ Element (ENumber (toVariable var) numform)
                         formatting

pLabel :: X.Element -> ElementParser (Element a)
pLabel node = do
  let attr = getAttributes node
  let formatting = getFormatting attr
  let variable = toVariable $ fromMaybe "" $ lookupAttribute "variable" attr
  let labelform = case lookupAttribute "form" attr of
                    Just "short"      -> Short
                    Just "verb"       -> Verb
                    Just "verb-short" -> VerbShort
                    Just "symbol"     -> Symbol
                    _                 -> Long
  let pluralize = case lookupAttribute "plural" attr of
                    Just "always" -> AlwaysPluralize
                    Just "never"  -> NeverPluralize
                    _             -> ContextualPluralize
  return $ Element (ELabel variable labelform pluralize) formatting

pNames :: X.Element -> ElementParser (Element a)
pNames node = do
  let attr = getAttributes node
  let formatting = getFormatting attr
  let variables = maybe [] splitVars $ lookupAttribute "variable" attr
  let pChild (nf,subst) n =
       case X.nameLocalName (X.elementName n) of
         "label"      -> do
           e <- pLabel n
           case e of
             Element (ELabel _ labelform pluralize) f ->
               return ( nf{ namesLabel = Just (labelform, pluralize, f)
                          , namesLabelBeforeName =
                              isNothing (namesName nf) }
                      , subst )
             _ -> parseFailure "pLabel returned something other than ELabel"
         "substitute" -> do
           els <- mapM pElement $ allChildren n
           return ( nf, els )
         "et-al"      -> do
           res <- pEtAl n
           return ( nf{ namesEtAl = Just res }, subst )
         "name"       -> do
           res <- pName n
           return ( nf{ namesName = Just res }, subst )
         name -> parseFailure $ "element " <> show name <>
                        " not a valid child of names"
  (nameformat, subst) <-
      foldM pChild (NamesFormat Nothing Nothing Nothing False, [])
                   (allChildren node)
  return $ Element (ENames variables nameformat subst) formatting

pEtAl :: X.Element -> ElementParser (Text, Formatting)
pEtAl node = do
  let attr = getAttributes node
  let formatting = getFormatting attr
  let term = fromMaybe "et-al" $ lookupAttribute "term" attr
  return (term, formatting)


pName :: X.Element -> ElementParser (NameFormat, Formatting)
pName node = do
  let attr = getAttributes node
  let formatting = getFormatting attr
  let nameParts = map getAttributes $ getChildren "name-part" node
  let nameformat = NameFormat
         { nameGivenFormatting        =
             case [nattr
                   | nattr <- nameParts
                   , lookupAttribute "name" nattr == Just "given" ] of
                (nattr:_) -> Just $ getFormatting nattr
                _     -> Nothing
         , nameFamilyFormatting       =
             case [nattr
                   | nattr <- nameParts
                   , lookupAttribute "name" nattr == Just "family" ] of
                (nattr:_) -> Just $ getFormatting nattr
                _     -> Nothing
         , nameAndStyle           =
             case lookupAttribute "and" attr of
               Just "text"   -> Just Long
               Just "symbol" -> Just Symbol
               _             -> Nothing
         , nameDelimiter              =
             lookupAttribute "delimiter" attr
         , nameDelimiterPrecedesEtAl  =
             case lookupAttribute "delimiter-precedes-et-al" attr of
               Just "after-inverted-name" -> Just PrecedesAfterInvertedName
               Just "always"              -> Just PrecedesAlways
               Just "never"               -> Just PrecedesNever
               Just "contextual"          -> Just PrecedesContextual
               _                          -> Nothing
         , nameDelimiterPrecedesLast  =
             case lookupAttribute "delimiter-precedes-last" attr of
               Just "after-inverted-name" -> Just PrecedesAfterInvertedName
               Just "always"              -> Just PrecedesAlways
               Just "never"               -> Just PrecedesNever
               Just "contextual"          -> Just PrecedesContextual
               _                          -> Nothing
         , nameEtAlMin                =
            lookupAttribute "et-al-min" attr >>= readAsInt
         , nameEtAlUseFirst           =
            lookupAttribute "et-al-use-first" attr >>= readAsInt
         , nameEtAlSubsequentUseFirst =
             lookupAttribute "et-al-subsequent-use-first" attr >>= readAsInt
         , nameEtAlSubsequentMin      =
             lookupAttribute "et-al-subsequent-min" attr >>= readAsInt
         , nameEtAlUseLast            =
             case lookupAttribute "et-al-use-last" attr of
               Just "true" -> Just True
               Just "false" -> Just False
               _           -> Nothing
         , nameForm                   =
             case lookupAttribute "form" attr of
               Just "short"  -> Just ShortName
               Just "count"  -> Just CountName
               Just "long"   -> Just LongName
               _             -> Nothing
         , nameInitialize             =
             case lookupAttribute "initialize" attr of
               Just "false" -> Just False
               Just "true" -> Just True
               _            -> Nothing
         , nameInitializeWith         =
             lookupAttribute "initialize-with" attr
         , nameAsSortOrder            =
             case lookupAttribute "name-as-sort-order" attr of
               Just "all"   -> Just NameAsSortOrderAll
               Just "first" -> Just NameAsSortOrderFirst
               _            -> Nothing
         , nameSortSeparator          =
             lookupAttribute "sort-separator" attr
         }
  return (nameformat, formatting)

pGroup :: X.Element -> ElementParser (Element a)
pGroup node = do
  let attr = getAttributes node
  let formatting = getFormatting attr
  es <- mapM pElement $ allChildren node
  return $ Element (EGroup False es) formatting

pText :: X.Element -> ElementParser (Element a)
pText node = do
  let attr = getAttributes node
  let formatting = getFormatting attr
  let varform = case lookupAttribute "form" attr of
                  Just "short" -> ShortForm
                  _            -> LongForm
  let termform = case lookupAttribute "form" attr of
                   Just "short"      -> Short
                   Just "verb"       -> Verb
                   Just "verb-short" -> VerbShort
                   Just "symbol"     -> Symbol
                   _                 -> Long
  let termnumber = case lookupAttribute "plural" attr of
                     Just "true"   -> Just Plural
                     Just "false"  -> Just Singular
                     _             -> Nothing
  elt <- case lookupAttribute "variable" attr of
           Just var -> return $ EText (TextVariable varform (toVariable var))
           Nothing ->
             case lookupAttribute "macro" attr of
               Just macroname -> return $ EText (TextMacro macroname)
               Nothing ->
                 case lookupAttribute "term" attr of
                   Just termname ->
                     return $ EText (TextTerm
                       Term { termName = termname
                            , termForm = termform
                            , termNumber = termnumber
                            , termGender = Nothing
                            , termGenderForm = Nothing
                            , termMatch = Nothing
                            })
                   Nothing ->
                     case lookupAttribute "value" attr of
                       Just val ->
                         return $ EText (TextValue val)
                       Nothing ->
                         parseFailure "text element lacks needed attribute"
  return $ Element elt formatting

pMacro :: X.Element -> ElementParser (Text, [Element a])
pMacro node = do
  name <- case lookupAttribute "name" (getAttributes node) of
            Just t  -> return t
            Nothing -> parseFailure "macro element missing name attribute"
  elts <- mapM pElement (allChildren node)
  return (name, elts)

getInheritableNameFormat :: Attributes -> NameFormat
getInheritableNameFormat attr =
  NameFormat
         { nameGivenFormatting = Nothing
         , nameFamilyFormatting = Nothing
         , nameAndStyle =
             case lookupAttribute "and" attr of
               Just "text"   -> Just Long
               Just "symbol" -> Just Symbol
               _             -> Nothing
         , nameDelimiter =
             lookupAttribute "name-delimiter" attr <|>
             lookupAttribute "names-delimiter" attr
         , nameDelimiterPrecedesEtAl  =
             case lookupAttribute "delimiter-precedes-et-al" attr of
               Just "after-inverted-name" -> Just PrecedesAfterInvertedName
               Just "always"              -> Just PrecedesAlways
               Just "never"               -> Just PrecedesNever
               Just "contextual"          -> Just PrecedesContextual
               _                          -> Nothing
         , nameDelimiterPrecedesLast  =
             case lookupAttribute "delimiter-precedes-last" attr of
               Just "after-inverted-name" -> Just PrecedesAfterInvertedName
               Just "always"              -> Just PrecedesAlways
               Just "never"               -> Just PrecedesNever
               Just "contextual"          -> Just PrecedesContextual
               _                          -> Nothing
         , nameEtAlMin                =
           lookupAttribute "et-al-min" attr >>= readAsInt
         , nameEtAlUseFirst           =
            lookupAttribute "et-al-use-first" attr >>= readAsInt
         , nameEtAlSubsequentUseFirst =
             lookupAttribute "et-al-subsequent-use-first" attr >>= readAsInt
         , nameEtAlSubsequentMin      =
             lookupAttribute "et-al-subsequent-min" attr >>= readAsInt
         , nameEtAlUseLast            =
             case lookupAttribute "et-al-use-last" attr of
               Just "true" -> Just True
               Just "false" -> Just False
               _           -> Nothing
         , nameForm                   =
             case lookupAttribute "name-form" attr of
               Just "short"  -> Just ShortName
               Just "count"  -> Just CountName
               Just "long"   -> Just LongName
               _             -> Nothing
         , nameInitialize             =
             case lookupAttribute "initialize" attr of
               Just "false" -> Just False
               Just "true" -> Just True
               _            -> Nothing
         , nameInitializeWith         =
             lookupAttribute "initialize-with" attr
         , nameAsSortOrder            =
             case lookupAttribute "name-as-sort-order" attr of
               Just "all"   -> Just NameAsSortOrderAll
               Just "first" -> Just NameAsSortOrderFirst
               _            -> Nothing
         , nameSortSeparator          =
             lookupAttribute "sort-separator" attr
         }

pLayout :: X.Element -> ElementParser (Layout a)
pLayout node = do
  let attr = getAttributes node
  let nameformat = getInheritableNameFormat attr
  let layouts = getChildren "layout" node
  -- In case there are multiple layouts (as CSL-M allows), we raise an error
  let elname = T.unpack $ X.nameLocalName $ X.elementName node
  layout <- case layouts of
              [] -> parseFailure $ "No layout element present in " <> elname
              [l] -> return l
              (_:_) -> parseFailure $ "Multiple layout elements present in " <> elname
  let formatting = getFormatting . getAttributes $ layout
  let sorts   = getChildren "sort" node
  elements <- mapM pElement $ allChildren layout
  let opts = LayoutOptions
             { layoutCollapse =
                 case lookupAttribute "collapse" attr of
                   Just "citation-number" -> Just CollapseCitationNumber
                   Just "year"            -> Just CollapseYear
                   Just "year-suffix"     -> Just CollapseYearSuffix
                   Just "year-suffix-ranged"
                                          -> Just CollapseYearSuffixRanged
                   _                      -> Nothing
             , layoutYearSuffixDelimiter =
                 lookupAttribute "year-suffix-delimiter" attr <|>
                   -- technically the spec doesn't say this, but
                   -- this seems to be what the test suites want?:
                   lookupAttribute "cite-group-delimiter" attr <|>
                   formatDelimiter formatting
             , layoutAfterCollapseDelimiter =
                 lookupAttribute "after-collapse-delimiter" attr <|>
                   formatDelimiter formatting
             , layoutNameFormat = nameformat
             }
  sortKeys <- mapM pSortKey (concatMap (getChildren "key") sorts)
  return $ Layout { layoutOptions  = opts
                  , layoutFormatting = formatting{
                                         formatAffixesInside = True }
                  , layoutElements = elements
                  , layoutSortKeys = sortKeys
                  }

pSortKey :: X.Element -> ElementParser (SortKey a)
pSortKey node = do
  let attr@(Attributes attr') = getAttributes node
  let direction = case lookupAttribute "sort" attr of
                    Just "descending" -> Descending
                    _                 -> Ascending
  -- The attributes names-min, names-use-first, and names-use-last may
  -- be used to override the values of the corresponding
  -- et-al-min/et-al-subsequent-min,
  -- et-al-use-first/et-al-subsequent-use-first and et-al-use-last
  -- attributes, and affect all names generated via macros called by
  -- cs:key.
  let keyChange "names-min" = "et-al-min"
      keyChange "names-use-first" = "et-al-use-first"
      keyChange "names-use-last" = "et-al-use-last"
      keyChange "names-subsequent-min" = "et-al-subsequent-min"
      keyChange "names-subsequent-use-first" = "et-al-subsequent-use-first"
      keyChange x = x
  let nameformat = getInheritableNameFormat
                     (Attributes (M.mapKeys keyChange attr'))
  case lookupAttribute "macro" attr of
      Just macroname -> return $ SortKeyMacro direction nameformat macroname
      Nothing   -> return $ SortKeyVariable direction
                     (toVariable $ fromMaybe mempty $
                       lookupAttribute "variable" attr)

splitVars :: Text -> [Variable]
splitVars = map toVariable . T.words . T.strip