summaryrefslogtreecommitdiff
path: root/walint/Properties.hs
diff options
context:
space:
mode:
Diffstat (limited to 'walint/Properties.hs')
-rw-r--r--walint/Properties.hs748
1 files changed, 748 insertions, 0 deletions
diff --git a/walint/Properties.hs b/walint/Properties.hs
new file mode 100644
index 0000000..7b5a181
--- /dev/null
+++ b/walint/Properties.hs
@@ -0,0 +1,748 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiWayIf #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- | Contains checks for custom ties of the map json
+module Properties (checkMap, checkTileset, checkLayer) where
+
+import Universum hiding (intercalate, isPrefixOf)
+
+import Data.Text (intercalate, isPrefixOf)
+import qualified Data.Text as T
+import Data.Tiled (Layer (..), Object (..), Property (..),
+ PropertyValue (..), Tile (..),
+ Tiledmap (..), Tileset (..))
+import Data.Tiled.Abstract (HasData (..), HasName (..),
+ HasProperties (..), HasTypeName (..),
+ IsProperty (..), layerIsEmpty)
+import qualified Data.Vector as V
+import Util (mkProxy, naiveEscapeHTML, prettyprint)
+
+import Badges (Badge (Badge),
+ BadgeArea (BadgePoint, BadgeRect),
+ BadgeToken, parseToken)
+import Data.List ((\\))
+import qualified Data.Set as S
+import Data.Text.Metrics (damerauLevenshtein)
+import GHC.TypeLits (KnownSymbol)
+import LayerData (Collision, layerOverlaps)
+import LintConfig (LintConfig (..))
+import LintWriter (LintWriter, adjust, askContext,
+ askFileDepth, complain, dependsOn, forbid,
+ lintConfig, offersBadge, offersCWs,
+ offersEntrypoint, offersJitsi, suggest,
+ warn, zoom)
+import Paths (PathResult (..), RelPath (..),
+ getExtension, isOldStyle, parsePath)
+import Types (Dep (Link, Local, LocalMap, MapLink))
+import Uris (SubstError (..), applySubsts)
+
+
+knownMapProperties :: Vector Text
+knownMapProperties = V.fromList
+ [ "mapName", "mapDescription", "mapCopyright", "mapLink", "script"
+ , "contentWarnings" ]
+
+knownTilesetProperties :: Vector Text
+knownTilesetProperties = V.fromList
+ [ "tilesetCopyright", "collides"]
+
+knownObjectProperties :: Vector Text
+knownObjectProperties = V.fromList
+ [ "name", "url", "getBadge", "soundRadius", "default", "persist", "openLayer"
+ , "closeLayer", "door", "bell", "openSound", "closeSound", "bellSound"
+ , "allowapi"]
+
+knownTileLayerProperites :: Vector Text
+knownTileLayerProperites = V.fromList
+ [ "jitsiRoom", "jitsiTrigger", "jitsiTriggerMessage", "jitsiWidth"
+ , "playAudio", "audioLoop", "audioVolumne"
+ , "openWebsite", "openWebsiteTrigger", "openWebsiteTriggerMessage", "openTag"
+ , "exitUrl", "startLayer", "silent", "getBadge", "zone", "name", "doorVariable"
+ , "bindVariable", "bellVariable", "code", "openTriggerMessage"
+ , "closeTriggerMessage", "autoOpen", "autoClose", "bellButtonText", "bellPopup"
+ , "enterValue", "leaveValue" ]
+
+-- | Checks an entire map for "general" lints.
+--
+-- Note that it does /not/ check any tile layer/tileset properties;
+-- these are handled seperately in CheckMap, since these lints go
+-- into a different field of the output.
+checkMap :: LintWriter Tiledmap
+checkMap = do
+ tiledmap <- askContext
+ let layers = collectLayers tiledmap
+ let unlessLayer = unlessElement layers
+
+ -- test custom map properties
+ mapM_ checkMapProperty (maybeToMonoid $ tiledmapProperties tiledmap)
+
+ -- can't have these with the rest of layer/tileset lints since they're
+ -- not specific to any one of them
+ refuseDoubledNames layers
+ refuseDoubledNames (tiledmapTilesets tiledmap)
+ refuseDoubledNames (getProperties tiledmap)
+
+ -- some layers should exist
+ unlessElementNamed layers "start"
+ $ complain "The map must have one layer named \"start\"."
+ unlessLayer (\l -> getName l == "floorLayer" && layerType l == "objectgroup")
+ $ complain "The map must have one layer named \"floorLayer\" of type \"objectgroup\"."
+ unlessLayer (`containsProperty` "exitUrl")
+ $ complain "The map must contain at least one layer with the property \"exitUrl\" set."
+
+ -- reject maps not suitable for workadventure
+ unless (tiledmapOrientation tiledmap == "orthogonal")
+ $ complain "The map's orientation must be set to \"orthogonal\"."
+ unless (tiledmapTileheight tiledmap == 32 && tiledmapTilewidth tiledmap == 32)
+ $ complain "The map's tile size must be 32 by 32 pixels."
+
+ unlessHasProperty "mapCopyright"
+ $ suggest "document the map's copyright via the \"mapCopyright\" property."
+
+ unlessHasProperty "contentWarnings"
+ $ suggest "set content warnings for your map via the \"contentWarnings\" property."
+
+ -- TODO: this doesn't catch collisions with the default start layer!
+ whenLayerCollisions layers (\(Property name _) -> name == "exitUrl" || name == "startLayer")
+ $ \cols -> warn $ "collisions between entry and / or exit layers: " <> prettyprint cols
+
+ let missingMetaInfo =
+ ["mapName","mapDescription","mapLink"]
+ \\ map getName (getProperties tiledmap)
+
+ unless (null missingMetaInfo)
+ $ suggest $ "consider adding meta information to your map using the "
+ <> prettyprint missingMetaInfo <> " properties."
+
+ where
+ -- recursively find all layers (to deal with nested group layers)
+ collectLayers :: Tiledmap -> V.Vector Layer
+ collectLayers tiledmap = tiledmapLayers tiledmap <>
+ V.fromList (concatMap groupmembers (tiledmapLayers tiledmap))
+ where groupmembers :: Layer -> [Layer]
+ groupmembers layer = concatMap groupmembers layers <> layers
+ where layers = fromMaybe [] $ layerLayers layer
+
+-- | Checks a single property of a map.
+checkMapProperty :: Property -> LintWriter Tiledmap
+checkMapProperty p@(Property name _) = case name of
+ "mapName" -> naiveEscapeProperty p
+ "mapDescription" -> naiveEscapeProperty p
+ "mapCopyright" -> naiveEscapeProperty p
+ "mapLink" -> pure ()
+ "contentWarnings" ->
+ unwrapString p $ \str -> do
+ offersCWs (T.splitOn "," str)
+ -- usually the linter will complain if names aren't in their
+ -- "canonical" form, but allowing that here so that multiple
+ -- scripts can be used by one map
+ _ | T.toLower name == "script" ->
+ unwrapURI (Proxy @"script") p
+ (dependsOn . Link)
+ (const $ forbid "scripts loaded from local files are disallowed")
+ | name `elem` ["jitsiRoom", "playAudio", "openWebsite"
+ , "url", "exitUrl", "silent", "getBadge"]
+ -> complain $ "property " <> name
+ <> " should be set on layers, not the map directly"
+ | otherwise
+ -> warnUnknown p knownMapProperties
+
+
+-- | check an embedded tileset.
+--
+-- Important to collect dependency files
+checkTileset :: LintWriter Tileset
+checkTileset = do
+ tileset <- askContext
+ case tilesetImage tileset of
+ Just str -> unwrapPath str (dependsOn . Local)
+ Nothing -> complain "Tileset does not refer to an image."
+
+ refuseDoubledNames (getProperties tileset)
+
+ -- reject tilesets unsuitable for workadventure
+ unless (tilesetTilewidth tileset == 32 && tilesetTileheight tileset == 32)
+ $ complain "Tilesets must have tile size 32x32."
+
+ when (tilesetImageheight tileset > 4096 || tilesetImagewidth tileset > 4096)
+ $ warn "Tilesets should not be larger than 4096x4096 pixels in total."
+
+ when (isJust (tilesetSource tileset))
+ $ complain "Tilesets must be embedded and cannot be loaded from external files."
+
+ unlessHasProperty "tilesetCopyright"
+ $ forbid "property \"tilesetCopyright\" for tilesets must be set."
+
+ when (isJust (tilesetFileName tileset))
+ $ complain "The \"filename\" property on tilesets was removed; use \"image\" instead (and perhaps a newer version of the Tiled Editor)."
+
+ -- check properties of individual tiles
+ tiles' <- forM (tilesetTiles tileset) $ mapM $ \tile -> do
+ mapM_ (checkTileProperty tile) (getProperties tile)
+ zoom (const tileset) (const tile) $ mapM_ (checkTileThing True) (getProperties tile)
+
+ adjust (\t -> t { tilesetTiles = tiles' })
+
+ -- check individual tileset properties
+ mapM_ checkTilesetProperty (maybeToMonoid $ tilesetProperties tileset)
+
+ case tilesetTiles tileset of
+ Nothing -> pure ()
+ Just tiles -> ifDoubledThings tileId
+ -- can't set properties on the same tile twice
+ (\tile -> complain $ "cannot set properties on the \
+ \tile with the id" <> show (tileId tile) <> "twice.")
+ tiles
+
+ where
+ checkTilesetProperty :: Property -> LintWriter Tileset
+ checkTilesetProperty p@(Property name _value) = case name of
+ "tilesetCopyright" -> naiveEscapeProperty p
+ "collides" -> warn "property \"collides\" should be set on individual tiles, not the tileset"
+ _ -> warn $ "unknown tileset property " <> prettyprint name
+
+ checkTileProperty :: Tile -> Property -> LintWriter Tileset
+ checkTileProperty tile p@(Property name _) =
+ case name of
+ "collides" -> isBool p
+ -- named tiles are needed for scripting and do not hurt otherwise
+ "name" -> isString p
+ "tilesetCopyright" -> warn "the \"tilesetCopyright\" property should be set on the entire tileset, \
+ \not an individual tile."
+ _ -> warnUnknown' ("unknown tile property "
+ <> prettyprint name <> " in tile with global id "
+ <> show (tileId tile)) p knownTilesetProperties
+
+
+-- | collect lints on a single map layer
+checkLayer :: LintWriter Layer
+checkLayer = do
+ layer <- askContext
+
+ refuseDoubledNames (getProperties layer)
+
+ when (isJust (layerImage layer))
+ $ complain "imagelayer are not supported."
+
+ case layerType layer of
+ "tilelayer" -> mapM_ (checkTileThing False) (getProperties layer)
+ "group" -> pure ()
+ "objectgroup" -> do
+
+ -- check object properties
+ objs <- forM (layerObjects layer) $ mapM $ \object -> do
+ -- this is a confusing constant zoom ...
+ zoom (const layer) (const object) $ mapM_ checkObjectProperty (getProperties object)
+ adjust (\l -> l { layerObjects = objs })
+
+ -- all objects which don't define badges
+ let publicObjects = map (V.filter (not . (`containsProperty` "getBadge"))) objs
+
+ -- remove badges from output
+ adjust $ \l -> l { layerObjects = publicObjects
+ , layerProperties = Nothing }
+
+ -- check layer properties
+ forM_ (getProperties layer) checkObjectGroupProperty
+
+ unless (layerName layer == "floorLayer") $
+ when (isNothing (layerObjects layer) || layerObjects layer == Just mempty) $
+ warn "objectgroup layer (which aren't the floorLayer) \
+ \are useless if they are empty."
+
+ ty -> complain $ "unsupported layer type " <> prettyprint ty <> "."
+
+ if layerType layer == "group"
+ then when (isNothing (layerLayers layer))
+ $ warn "Empty group layers are pointless."
+ else when (isJust (layerLayers layer))
+ $ complain "Layer is not of type \"group\", but has sublayers."
+
+checkObjectProperty :: Property -> LintWriter Object
+checkObjectProperty p@(Property name _) = do
+ obj <- askContext
+ case name of
+ "url" -> do
+ unwrapURI (Proxy @"website") p
+ (dependsOn . Link)
+ (const $ forbid "using \"url\" to open local html files is disallowed.")
+ unless (objectType obj == "website")
+ $ complain "\"url\" can only be set for objects of type \"website\""
+ "getBadge" -> do
+ when (1 /= length (getProperties obj))
+ $ warn "Objects with the property \"getBadge\" set are removed at runtime, \
+ \and any other properties set on them will be gone."
+ unwrapString p $ \str ->
+ unwrapBadgeToken str $ \token -> do
+ case obj of
+ ObjectPolygon {} -> complain "polygons are not supported."
+ ObjectPolyline {} -> complain "polylines are not supported."
+ ObjectText {} -> complain "cannot use texts to define badge areas."
+ ObjectRectangle {..} ->
+ if objectEllipse == Just True
+ then complain "ellipses are not supported."
+ else offersBadge
+ $ Badge token $ case (objectWidth, objectHeight) of
+ (Just w, Just h) | w /= 0 && h /= 0 ->
+ BadgeRect objectX objectY w h
+ _ -> BadgePoint objectX objectY
+ "soundRadius" -> do
+ isIntInRange 0 maxBound p
+ unless (containsProperty obj "door" || containsProperty obj "bell")
+ $ complain "property \"soundRadius\" can only be set on objects with \
+ \either property \"bell\" or \"door\" also set."
+
+ _ | name `elem` [ "default", "persist" ] ->
+ suggestPropertyName' "door"
+ -- extended API for doors and bells
+ | name `elem` [ "openLayer", "closeLayer" ] -> do
+ isString p
+ suggestPropertyName' "door"
+ -- extended API for doors and bells
+ | name `elem` ["door", "bell"] -> do
+ isBool p
+ unless (objectType obj == "variable") $
+ complain $ "the "<>prettyprint name<>" property should only be set \
+ \on objects of type \"variable\""
+ when (isNothing (objectName obj) || objectName obj == Just mempty) $
+ complain $ "Objects with the property "<>prettyprint name<>" set must \
+ \be named."
+ | name `elem` [ "openSound", "closeSound", "bellSound", "loadSound" ] -> do
+ isString p
+ unwrapURI (Proxy @"audio") p
+ (dependsOn . Link)
+ (dependsOn . Local)
+ case name of
+ "bellSound" ->
+ suggestPropertyName' "bell"
+ "closeSound" | containsProperty obj "openSound" ->
+ suggestPropertyName' "door"
+ _ -> do
+ suggestPropertyName' "door"
+ suggestPropertyName "soundRadius"
+ "set \"soundRadius\" to limit the door sound to a certain area."
+ | T.toLower name == "allowapi"
+ -> forbidProperty name
+ | otherwise ->
+ warnUnknown p knownObjectProperties
+
+-- | Checks a single (custom) property of an objectgroup layer
+checkObjectGroupProperty :: Property -> LintWriter Layer
+checkObjectGroupProperty (Property name _) = case name of
+ "getBadge" -> warn "the property \"getBadge\" must be set on individual objects, \
+ \not the object layer."
+ _ -> warn $ "unknown property " <> prettyprint name <> " for objectgroup layers"
+
+
+
+-- | Checks a single (custom) property. Since almost all properties
+-- can be set on tile layer AND on tilesets, this function aims to
+-- be generic over both — the only difference is that tilesets can't
+-- have exits, which is specified by the sole boolean argument
+checkTileThing
+ :: (HasProperties a, HasName a, HasData a)
+ => Bool -> Property -> LintWriter a
+checkTileThing removeExits p@(Property name _value) = case name of
+ "jitsiRoom" -> do
+ uselessEmptyLayer
+ -- members of an assembly should automatically get
+ -- admin rights in jitsi (prepending "assembly-" here
+ -- to avoid namespace clashes with other admins)
+ lintConfig configAssemblyTag
+ >>= setProperty "jitsiRoomAdminTag"
+ . ("assembly-" <>)
+ unwrapString p $ \jitsiRoom -> do
+ suggestProperty $ Property "jitsiTrigger" "onaction"
+
+ -- prevents namespace clashes for jitsi room names
+ if not ("shared" `isPrefixOf` jitsiRoom) then do
+ assemblyname <- lintConfig configAssemblyTag
+ setProperty "jitsiRoom" (assemblyname <> "-" <> jitsiRoom)
+ offersJitsi (assemblyname <> "-" <> jitsiRoom)
+ else
+ offersJitsi jitsiRoom
+ "jitsiTrigger" -> do
+ isString p
+ unlessHasProperty "jitsiTriggerMessage"
+ $ suggest "set \"jitsiTriggerMessage\" to a custom message to overwrite \
+ \the default \"press SPACE to enter in jitsi meet room\"."
+ requireProperty "jitsiRoom"
+ "jitsiTriggerMessage" -> do
+ isString p
+ requireProperty "jitsiTrigger"
+ "jitsiWidth" ->
+ isIntInRange 0 100 p
+ "playAudio" -> do
+ uselessEmptyLayer
+ unwrapURI (Proxy @"audio") p
+ (dependsOn . Link)
+ (dependsOn . Local)
+ "audioLoop" -> do
+ isBool p
+ requireProperty "playAudio"
+ "playAudioLoop" ->
+ deprecatedUseInstead "audioLoop"
+ "audioVolume" -> do
+ isOrdInRange unwrapFloat 0 1 p
+ requireProperty "playAudio"
+ "openWebsiteTrigger" -> do
+ isString p
+ requireOneOf ["openWebsite", "openTab"]
+ unlessHasProperty "openWebsiteTriggerMessage"
+ $ suggest "set \"openWebsiteTriggerMessage\" to a custom message to \
+ \overwrite the default \"press SPACE to open Website\"."
+ "openWebsiteTriggerMessage" -> do
+ isString p
+ requireProperty "openWebsiteTrigger"
+ "url" -> complain "the property \"url\" defining embedded iframes must be \
+ \set on an object in an objectgroup layer."
+ "exitUrl" -> if not removeExits
+ then do
+ forbidEmptyLayer
+ unwrapURI (Proxy @"map") p
+ (\link -> do
+ assemblyslug <- lintConfig configAssemblyTag
+ eventslug <- lintConfig configEventSlug
+ case T.stripPrefix ("/@/"<>eventslug<>"/"<>assemblyslug<>"/") link of
+ Nothing -> do
+ dependsOn (MapLink link)
+ setProperty "exitUrl" link
+ Just path -> case parsePath path of
+ OkRelPath (Path _ p frag) -> do
+ up <- askFileDepth
+ dependsOn (LocalMap (Path up p frag))
+ setProperty "exitUrl" path
+ warn "You should use relative links to your own assembly instead \
+ \of world://-style links (I've tried to adjust them \
+ \automatically for now)."
+ _ -> complain "There's a path I don't understand here. Perhaps try \
+ \asking a human?"
+ )
+ ( \path ->
+ let ext = getExtension path in
+ if | isOldStyle path -> do
+ eventslug <- lintConfig configEventSlug
+ complain $
+ "Old-Style inter-repository links (using {<placeholder>}) \
+ \cannot be used at "<>eventslug<>"; please use world:// \
+ \instead (see the howto)."
+ | ext == "tmx" ->
+ complain "Cannot use .tmx map format; use Tiled's json export instead."
+ | ext /= "json" ->
+ complain "All exit links must link to .json files."
+ | otherwise -> dependsOn . LocalMap $ path
+ )
+ else do
+ warn "exitUrls in Tilesets are not unsupported; if you want to \
+ \add an exit, please use a tile layer instead."
+ "exitSceneUrl" ->
+ deprecatedUseInstead "exitUrl"
+ "exitInstance" ->
+ deprecatedUseInstead "exitUrl"
+ "startLayer" -> do
+ forbidEmptyLayer
+ layer <- askContext
+ unwrapBool p $ \case
+ True -> offersEntrypoint $ getName layer
+ False -> warn "property \"startLayer\" is useless if set to false."
+ "silent" -> do
+ isBool p
+ uselessEmptyLayer
+ "getBadge" -> complain "\"getBadge\" must be set on an \"objectgroup\" \
+ \ layer; it does not work on tile layers."
+
+ -- extended API stuff
+ "zone" -> do
+ isString p
+ uselessEmptyLayer
+ -- name on tile layer unsupported
+ "name" -> isUnsupported
+ _ | name `elem` [ "doorVariable", "bindVariable", "bellVariable" ]
+ -> do { isString p; requireProperty "zone" }
+ | name `elem` [ "code", "openTriggerMessage", "closeTriggerMessage"]
+ -> do { isString p; requireProperty "doorVariable" }
+ | name `elem` [ "autoOpen", "autoClose"]
+ -> do { isBool p; requireProperty "doorVariable" }
+ | name `elem` [ "bellButtonText", "bellPopup" ]
+ -> do { isString p; requireProperty "bellVariable" }
+ | name `elem` [ "enterValue", "leaveValue" ]
+ -> do { isString p; requireProperty "bindVariable" }
+ | T.toLower name `elem` [ "jitsiurl", "jitsiconfig", "jitsiclientconfig"
+ , "jitsiroomadmintag", "jitsiinterfaceconfig"
+ , "openwebsitepolicy", "allowapi" ]
+ -> forbidProperty name
+ | name `elem` [ "openWebsite", "openTab" ] -> do
+ uselessEmptyLayer
+ suggestProperty $ Property "openWebsiteTrigger" "onaction"
+
+ properties <- askContext <&> getProperties
+ let isScript = any (\(Property name _) ->
+ T.toLower name == "openwebsiteallowapi")
+ properties
+ if isScript
+ then unwrapURI (Proxy @"script") p
+ (dependsOn . Link)
+ (const $ forbid "accessing local html files is disallowed")
+ else unwrapURI (Proxy @"website") p
+ (dependsOn . Link)
+ (const $ forbid "accessing local html files is disallowed.")
+ | otherwise ->
+ when (not removeExits || name `notElem` [ "collides", "name", "tilesetCopyright" ]) $ do
+ warnUnknown p knownTileLayerProperites
+ where
+ requireProperty req = propertyRequiredBy req name
+ requireOneOf names = do
+ context <- askContext
+ unless (any (containsProperty context) names)
+ $ complain $ "property " <> prettyprint name <> " requires one of "
+ <> prettyprint names
+
+ isUnsupported = warn $ "property " <> name <> " is not (yet) supported by walint."
+ deprecatedUseInstead instead =
+ warn $ "property \"" <> name <> "\" is deprecated. Use \"" <> instead <> "\" instead."
+
+ -- | this property can only be used on a layer that contains
+ -- | at least one tile
+ forbidEmptyLayer = when removeExits $ do
+ layer <- askContext
+ when (layerIsEmpty layer)
+ $ complain ("property " <> prettyprint name <> " should not be set on an empty layer.")
+
+ -- | this layer is allowed, but also useless on a layer that contains no tiles
+ uselessEmptyLayer = when removeExits $ do
+ layer <- askContext
+ when (layerIsEmpty layer)
+ $ warn ("property " <> prettyprint name <> " set on an empty layer is useless.")
+
+
+-- | refuse doubled names in everything that's somehow a collection of names
+refuseDoubledNames
+ :: (Container t, HasName (Element t), HasTypeName (Element t))
+ => t
+ -> LintWriter b
+refuseDoubledNames = ifDoubledThings getName
+ (\thing -> complain $ "cannot use " <> typeName (mkProxy thing) <> " name "
+ <> getName thing <> " multiple times.")
+
+-- | do `ifDouble` if any element of `things` occurs more than once under
+-- the function `f`
+ifDoubledThings
+ :: (Eq a, Ord a, Container t)
+ => (Element t -> a)
+ -> (Element t -> LintWriter b)
+ -> t
+ -> LintWriter b
+ifDoubledThings f ifDouble things = foldr folding base things (mempty, mempty)
+ where
+ folding thing cont (seen, twice)
+ | f thing `elem` seen && f thing `notElem` twice = do
+ ifDouble thing
+ cont (seen, S.insert (f thing) twice)
+ | otherwise =
+ cont (S.insert (f thing) seen, twice)
+ base _ = pure ()
+
+-- | we don't know this property; give suggestions for ones with similar names
+warnUnknown' :: Text -> Property -> Vector Text -> LintWriter a
+warnUnknown' msg (Property name _) knowns =
+ if snd minDist < 4
+ then warn (msg <> ", perhaps you meant " <> prettyprint (fst minDist) <> "?")
+ else warn msg
+ where dists = V.map (\n -> (n, damerauLevenshtein name n)) knowns
+ minDist = V.minimumBy (\(_,a) (_,b) -> compare a b) dists
+
+warnUnknown :: Property -> Vector Text -> LintWriter a
+warnUnknown p@(Property name _) =
+ warnUnknown' ("unknown property " <> prettyprint name) p
+
+---- General functions ----
+
+unlessElement
+ :: Container f
+ => f
+ -> (Element f -> Bool)
+ -> LintWriter b
+ -> LintWriter b
+unlessElement things op = unless (any op things)
+
+unlessElementNamed :: (HasName (Element f), Container f)
+ => f -> Text -> LintWriter b -> LintWriter b
+unlessElementNamed things name =
+ unlessElement things ((==) name . getName)
+
+unlessHasProperty :: HasProperties a => Text -> LintWriter a -> LintWriter a
+unlessHasProperty name linter =
+ askContext >>= \ctxt ->
+ unlessElementNamed (getProperties ctxt) name linter
+
+-- | does this layer have the given property?
+containsProperty :: HasProperties a => a -> Text -> Bool
+containsProperty thing name = any
+ (\(Property name' _) -> name' == name) (getProperties thing)
+
+-- | should the layers fulfilling the given predicate collide, then perform andthen.
+whenLayerCollisions
+ :: V.Vector Layer
+ -> (Property -> Bool)
+ -> (Set Collision -> LintWriter a)
+ -> LintWriter a
+whenLayerCollisions layers f andthen = do
+ let collisions = layerOverlaps . V.filter (any f . getProperties) $ layers
+ unless (null collisions)
+ $ andthen collisions
+
+----- Functions with concrete lint messages -----
+
+-- | this property is forbidden and should not be used
+forbidProperty :: HasProperties a => Text -> LintWriter a
+forbidProperty name =
+ forbid $ "property " <> prettyprint name <> " is disallowed."
+
+propertyRequiredBy :: HasProperties a => Text -> Text -> LintWriter a
+propertyRequiredBy req by =
+ unlessHasProperty req
+ $ complain $ "property " <> prettyprint req <>
+ " is required by property " <> prettyprint by <> "."
+
+-- | suggest some value for another property if that property does not
+-- also already exist
+suggestProperty :: HasProperties a => Property -> LintWriter a
+suggestProperty p@(Property name value) =
+ suggestProperty' p $ "add property " <> prettyprint name <> " to \"" <> prettyprint value<>"\"."
+
+suggestProperty' :: HasProperties a => Property -> Text -> LintWriter a
+suggestProperty' (Property name _) msg =
+ unlessHasProperty name (suggest msg)
+
+suggestPropertyName :: HasProperties a => Text -> Text -> LintWriter a
+suggestPropertyName name msg =
+ unlessHasProperty name (suggest msg)
+
+suggestPropertyName' :: HasProperties a => Text -> LintWriter a
+suggestPropertyName' name = suggestPropertyName name
+ $ "consider setting property " <> prettyprint name <> "."
+
+---- Functions for adjusting the context -----
+
+
+-- | set a property, overwriting whatever value it had previously
+setProperty :: (IsProperty prop, HasProperties ctxt)
+ => Text -> prop -> LintWriter ctxt
+setProperty name value = adjust $ \ctxt ->
+ flip adjustProperties ctxt
+ $ \ps -> Just $ Property name (asProperty value) : filter sameName ps
+ where sameName (Property name' _) = name /= name'
+
+naiveEscapeProperty :: HasProperties a => Property -> LintWriter a
+naiveEscapeProperty prop@(Property name _) =
+ unwrapString prop (setProperty name . naiveEscapeHTML)
+
+---- "unwrappers" checking that a property has some type, then do something ----
+
+-- | asserts that this property is a string, and unwraps it
+unwrapString :: Property -> (Text -> LintWriter a) -> LintWriter a
+unwrapString (Property name value) f = case value of
+ StrProp str -> f str
+ _ -> complain $ "type error: property "
+ <> prettyprint name <> " should be of type string."
+
+
+-- | asserts that this property is a boolean, and unwraps it
+unwrapBool :: Property -> (Bool -> LintWriter a) -> LintWriter a
+unwrapBool (Property name value) f = case value of
+ BoolProp b -> f b
+ _ -> complain $ "type error: property " <> prettyprint name
+ <> " should be of type bool."
+
+unwrapInt :: Property -> (Int -> LintWriter a) -> LintWriter a
+unwrapInt (Property name value) f = case value of
+ IntProp float -> f float
+ _ -> complain $ "type error: property " <> prettyprint name
+ <> " should be of type int."
+
+unwrapFloat :: Property -> (Float -> LintWriter a) -> LintWriter a
+unwrapFloat (Property name value) f = case value of
+ FloatProp float -> f float
+ _ -> complain $ "type error: property " <> prettyprint name
+ <> " should be of type float."
+
+unwrapPath :: Text -> (RelPath -> LintWriter a) -> LintWriter a
+unwrapPath str f = case parsePath str of
+ OkRelPath p@(Path up _ _) -> do
+ depth <- askFileDepth
+ if up <= depth
+ then f p
+ else complain $ "cannot acess paths \"" <> str <> "\" which is outside your repository."
+ NotAPath -> complain $ "path \"" <> str <> "\" is invalid."
+ AbsolutePath -> forbid "absolute paths are disallowed. Use world:// instead."
+ UnderscoreMapLink -> forbid "map links using /_/ are disallowed. Use world:// instead."
+ AtMapLink -> forbid "map links using /@/ are disallowed. Use world:// instead."
+ PathVarsDisallowed -> forbid "extended API variables are not allowed in asset paths."
+
+unwrapBadgeToken :: Text -> (BadgeToken -> LintWriter a) -> LintWriter a
+unwrapBadgeToken str f = case parseToken str of
+ Just a -> f a
+ Nothing -> complain "invalid badge token."
+
+
+-- | unwraps a link, giving two cases:
+-- - the link might be an (allowed) remote URI
+-- - the link might be relative to this map (i.e. just a filepath)
+unwrapURI :: (KnownSymbol s, HasProperties a)
+ => Proxy s
+ -> Property
+ -> (Text -> LintWriter a)
+ -> (RelPath -> LintWriter a)
+ -> LintWriter a
+unwrapURI sym p@(Property name _) f g = unwrapString p $ \link -> do
+ subst <- lintConfig configUriSchemas
+ case applySubsts sym subst link of
+ Right uri -> do
+ setProperty name uri
+ f uri
+ Left NotALink -> unwrapPath link g
+ Left err -> do
+ isLobby <- lintConfig configAssemblyTag <&> (== "lobby")
+
+ (if isLobby then warn else complain) $ case err of
+ DomainIsBlocked domains -> link <> " is a blocked site; links in this \
+ \context may link to " <> prettyprint domains
+ IsBlocked -> link <> " is blocked."
+ DomainDoesNotExist domain -> "The domain " <> domain <> " does not exist; \
+ \please make sure it is spelled correctly."
+ SchemaDoesNotExist schema ->
+ "the URI schema " <> schema <> "// cannot be used."
+ WrongScope schema allowed ->
+ "the URI schema " <> schema <> "// cannot be used in property \
+ \\"" <> name <> "\"; allowed "
+ <> (if length allowed == 1 then "is " else "are ")
+ <> intercalate ", " (map (<> "//") allowed) <> "."
+ VarsDisallowed -> "extended API links are disallowed in links"
+
+
+
+-- | just asserts that this is a string
+isString :: Property -> LintWriter a
+isString = flip unwrapString (const $ pure ())
+
+-- | just asserts that this is a boolean
+isBool :: Property -> LintWriter a
+isBool = flip unwrapBool (const $ pure ())
+
+isIntInRange :: Int -> Int -> Property -> LintWriter b
+isIntInRange = isOrdInRange @Int unwrapInt
+
+isOrdInRange :: (Ord a, Show a)
+ => (Property -> (a -> LintWriter b) -> LintWriter b)
+ -> a
+ -> a
+ -> Property
+ -> LintWriter b
+isOrdInRange unwrapa l r p@(Property name _) = unwrapa p $ \int ->
+ if l < int && int < r then pure ()
+ else complain $ "Property " <> prettyprint name <> " should be between "
+ <> show l <> " and " <> show r<>"."