packages feed

casa-reoc-0.1.0.0: src/ReOC/Render/TrainingManual/Markup.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wall #-}

{- | The marked-up text of the RePL training manual template: markdown with
holes @{{name}}@ and markers @<!-- ... -->@ for text that is repeated for
each course or included on a condition.
-}
module ReOC.Render.TrainingManual.Markup (
  parseMarkup,
  checkMarkup,
  conditionValues,
) where

import Data.Text (Text)
import qualified Data.Text as Text
import ReOC.Render.TrainingManual.Chunk (Chunk (..))

{- $setup
>>> :set -XOverloadedStrings
-}

-- A token of the marked-up text.
data Token
  = TText Text
  | THole Text
  | TMarker Text

tokens :: Text -> [Token]
tokens t
  | Text.null t = []
  | otherwise =
      let (before, rest) = breakFirst t
       in [TText before | not (Text.null before)] <> marker rest
 where
  breakFirst x =
    let (a, _) = Text.breakOn "{{" x
        (b, _) = Text.breakOn "<!--" x
     in if Text.length a <= Text.length b then Text.splitAt (Text.length a) x else Text.splitAt (Text.length b) x
  marker rest
    | "{{" `Text.isPrefixOf` rest =
        let (name, after) = Text.breakOn "}}" (Text.drop 2 rest)
         in THole name : tokens (Text.drop 2 after)
    | "<!--" `Text.isPrefixOf` rest =
        let (body, after) = Text.breakOn "-->" (Text.drop 4 rest)
         in TMarker (Text.strip body) : tokens (Text.drop 3 after)
    | otherwise = []

{- | Parses marked-up text.

>>> parseMarkup "a {{organisation-name}} <!-- if simulator -->b<!-- end -->"
Right [Literal "a ",Hole "organisation-name",Literal " ",SimulatorCondition [Literal "b"]]

>>> parseMarkup "<!-- if simulator -->b"
Left "a marker is not closed"
-}
parseMarkup :: Text -> Either String [Chunk]
parseMarkup t =
  case go (tokens t) of
    Right (cs, []) -> Right cs
    Right (_, _ : _) -> Left "an end marker has no start"
    Left e -> Left e
 where
  -- parses chunks up to an end marker, which is returned with the tokens after it
  go :: [Token] -> Either String ([Chunk], [Token])
  go = \case
    [] -> Right ([], [])
    TText x : rest -> first (Literal x :) <$> go rest
    THole n : rest -> first (Hole n :) <$> go rest
    ts@(TMarker m : rest)
      | m == "end" || m == "per-course end" -> Right ([], ts)
      | m == "per-course begin" -> block "per-course end" PerCourse rest
      | m == "if simulator" -> block "end" SimulatorCondition rest
      | Just c <- Text.stripPrefix "if offered " m -> condition OfferedCondition c rest
      | Just c <- Text.stripPrefix "if " m -> condition CourseCondition c rest
      | otherwise -> Left ("unknown marker: " <> Text.unpack m)
  block close make rest = do
    (inner, after) <- go rest
    case after of
      TMarker m : more | m == close -> first (make inner :) <$> go more
      _ -> Left "a marker is not closed"
  condition make c rest =
    case Text.splitOn "=" c of
      [k, v] -> block "end" (make k v) rest
      _ -> Left ("not a condition: " <> Text.unpack c)
  first f (a, b) = (f a, b)

{- | The values of a key of a condition: @category@, @weight@, @complexity@ or
@course-type@.
-}
conditionValues :: Text -> Maybe [Text]
conditionValues = \case
  "category" -> Just ["multirotor", "helicopter", "powered-lift", "aeroplane"]
  "weight" -> Just ["up-to-7kg", "up-to-25kg"]
  "complexity" -> Just ["automated-and-manual", "automated-only"]
  "course-type" -> Just ["initial", "upgrade-same-category", "upgrade-different-category"]
  _ -> Nothing

{- | The errors of marked-up text, given the names of the holes of the
manual and of each course: unknown holes, unknown conditions, course holes
and conditions outside a @per-course@ block, and nested @per-course@ blocks.

>>> checkMarkup ["organisation-name"] ["course-title"] [Hole "course-title"]
["the hole {{course-title}} is outside a per-course block"]
-}
checkMarkup :: [Text] -> [Text] -> [Chunk] -> [String]
checkMarkup global course =
  concatMap (check False)
 where
  check inCourse = \case
    Literal _ -> []
    Hole n
      | n `elem` global -> []
      | n `elem` course -> ["the hole {{" <> Text.unpack n <> "}} is outside a per-course block" | not inCourse]
      | otherwise -> ["unknown hole {{" <> Text.unpack n <> "}}"]
    CourseCondition k v cs ->
      ["the condition " <> Text.unpack k <> "=" <> Text.unpack v <> " is outside a per-course block" | not inCourse]
        <> value k v
        <> concatMap (check inCourse) cs
    OfferedCondition k v cs ->
      value k v <> concatMap (check inCourse) cs
    SimulatorCondition cs ->
      concatMap (check inCourse) cs
    PerCourse cs ->
      ["a per-course block is inside another" | inCourse] <> concatMap (check True) cs
  value k v =
    case conditionValues k of
      Nothing -> ["unknown condition key: " <> Text.unpack k]
      Just vs -> ["unknown value of " <> Text.unpack k <> ": " <> Text.unpack v | v `notElem` vs]