packages feed

miso-1.12.0.0: src/Miso/JSON/Lexer.hs

----------------------------------------------------------------------------
{-# LANGUAGE CPP               #-}
{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Miso.JSON.Lexer
-- Copyright   :  (C) 2016-2026 David M. Johnson
-- License     :  BSD3-style (see the file LICENSE)
-- Maintainer  :  David M. Johnson <code@dmj.io>
-- Stability   :  experimental
-- Portability :  non-portable
--
-- = Overview
--
-- "Miso.JSON.Lexer" is the first stage of miso's pure Haskell JSON pipeline,
-- which is used for server-side rendering (SSR). It tokenises a
-- 'Miso.String.MisoString' into a stream of 'Token' values consumed by
-- "Miso.JSON.Parser".
--
-- This module is __internal__. Application code should use "Miso.JSON" or
-- "Miso.JSON.Parser" ('Miso.JSON.Parser.decodePure') instead.
--
-- This module was ported from <https://github.com/dmjio/json-test> by
-- <https://github.com/ners @ners>.
--
-- = Token types
--
-- @
-- data 'Token'
--   = 'TokenPunctuator' Char    -- one of @[ ] { } , :@
--   | 'TokenNumber'     Double  -- JSON number (integer or floating-point)
--   | 'TokenBool'       Bool    -- @true@ or @false@
--   | 'TokenString'     'Miso.String.MisoString' -- quoted string with escape sequences
--   | 'TokenNull'               -- @null@
-- @
--
-- String tokens handle all
-- <https://www.rfc-editor.org/rfc/rfc8259#section-7 RFC 8259 escape sequences>
-- including @\\uXXXX@ and UTF-16 surrogate pairs (@\\uD800\\uDC00@).
--
-- = See also
--
-- * "Miso.JSON.Parser" — consumes 'Token' streams produced here
-- * "Miso.JSON.Types" — 'Miso.JSON.Types.Value' produced by the parser
-- * "Miso.Util.Lexer" — the underlying 'Miso.Util.Lexer.Lexer' combinator library
----------------------------------------------------------------------------
module Miso.JSON.Lexer (Token (..), tokens) where
----------------------------------------------------------------------------
import           Control.Applicative (Alternative (some, many), optional)
import           Control.Monad (replicateM)
import           Data.Char (isHexDigit, chr, isSpace)
import           Data.Foldable (Foldable (fold))
import           Data.Functor (void)
import           Data.Ix (Ix (inRange))
import           Data.Maybe (catMaybes)
import           Numeric (readHex)
import           Prelude hiding (null)
----------------------------------------------------------------------------
import           Miso.String (fromMisoString, ToMisoString (toMisoString), MisoString)
import           Miso.Util (oneOf)
import           Miso.Util.Lexer hiding (string', token)
----------------------------------------------------------------------------
#if __GLASGOW_HASKELL__ <= 881
import Control.Applicative (liftA2)
#endif
----------------------------------------------------------------------------
data Token
  = TokenPunctuator Char
  | TokenNumber Double
  | TokenBool Bool
  | TokenString MisoString
  | TokenNull
  deriving (Eq, Show)
----------------------------------------------------------------------------
number :: Lexer Double
number = fromMisoString . fold . catMaybes <$> sequence
  [ optional $ string "-"
  , Just <$> int
  , optional $ liftA2 (<>) (string ".") int
  , optional $ liftA2 (<>) (oneOf $ string <$> ["e", "e+", "e-", "E", "E+", "E-"]) int
  ] where
      digit = satisfy $ inRange ('0', '9')
      int = toMisoString <$> some digit
----------------------------------------------------------------------------
bool :: Lexer Bool
bool = oneOf
  [ False <$ string "false"
  , True <$ string "true"
  ]
----------------------------------------------------------------------------
string' :: Lexer MisoString
string' = char '"' *> (toMisoString <$> many character) <* char '"'
  where
    character = oneOf
      [ satisfy $ \c -> c /= '"' && c /= '\\'
      , escapedCharacter
      ]
    hexDigit = satisfy isHexDigit
    escaped = (char '\\' *>)
    escapedCharacter = escaped $ oneOf
      [ char '"'
      , char '\\'
      , char '/'
      , '\b' <$ char 'b'
      , '\f' <$ char 'f'
      , '\n' <$ char 'n'
      , '\r' <$ char 'r'
      , '\t' <$ char 't'
      , unicodeHexQuad >>= \high -> do
          if inRange highSurrogateRange high
            then do
              low <- escaped unicodeHexQuad
              if inRange lowSurrogateRange low
                then
                  pure . chr . sum $
                    [ (high - fst highSurrogateRange) * 0x400
                    , low - fst lowSurrogateRange
                    , 0x10000
                    ]
                else oops
            else
              pure $ chr high
      ]
    highSurrogateRange = (0xD800, 0xDBFF)
    lowSurrogateRange = (0xDC00, 0xDFFF)
    unicodeHexQuad = char 'u' *> do
        [(num, "")] <- readHex <$> replicateM 4 hexDigit
        pure num
----------------------------------------------------------------------------
null :: Lexer ()
null = void (string "null")
----------------------------------------------------------------------------
punctuator :: Lexer Char
punctuator = oneOf (char <$> "[]{},:")
----------------------------------------------------------------------------
whitespace :: Lexer ()
whitespace = void (satisfy isSpace)
----------------------------------------------------------------------------
token :: Lexer Token
token = oneOf
  [ TokenPunctuator <$> punctuator
  , TokenNumber <$> number
  , TokenBool <$> bool
  , TokenString <$> string'
  , TokenNull <$ null
  ]
----------------------------------------------------------------------------
tokens :: Lexer [Token]
tokens = some (many whitespace *> token)
----------------------------------------------------------------------------