damnpacket 0.6.0 → 1.0.0
raw patch · 16 files changed
+648/−380 lines, 16 filesdep +HUnitdep +faildep +hspecdep −containersdep −criteriondep −deepseqdep ~QuickCheckdep ~attoparsecdep ~basenew-uploader
Dependencies added: HUnit, fail, hspec, html-entities, template-haskell
Dependencies removed: containers, criterion, deepseq
Dependency ranges changed: QuickCheck, attoparsec, base, bytestring, text
Files
- ChangeLog.md +5/−0
- LICENSE +17/−18
- benchmarks/parse.hs +0/−54
- damnpacket.cabal +34/−39
- src/Network/Damn.hs +226/−0
- src/Network/Damn/Format/Base.hs +10/−0
- src/Network/Damn/Format/Damn.hs +23/−0
- src/Network/Damn/Format/Damn/Internal.hs +63/−0
- src/Network/Damn/Format/IRC.hs +41/−0
- src/Network/Damn/Tablumps.hs +129/−0
- src/Network/Damn/Tablumps/TH.hs +74/−0
- src/Text/Damn/Packet.hs +0/−27
- src/Text/Damn/Packet/Internal.hs +0/−94
- src/Text/Damn/Packet/Parser.hs +0/−112
- test/parse.hs +26/−0
- tests/render.hs +0/−36
+ ChangeLog.md view
@@ -0,0 +1,5 @@+# Revision history for damn++## 0.1.0.0 -- YYYY-mm-dd++* First version. Released on an unsuspecting world.
LICENSE view
@@ -1,21 +1,20 @@-The MIT License (MIT)--Copyright (c) 2013 Joel Taylor+Copyright (c) 2016 Jude Taylor -Permission is hereby granted, free of charge, to any person obtaining a copy-of this software and associated documentation files (the "Software"), to deal-in the Software without restriction, including without limitation the rights-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell-copies of the Software, and to permit persons to whom the Software is-furnished to do so, subject to the following conditions:+Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions: -The above copyright notice and this permission notice shall be included in-all copies or substantial portions of the Software.+The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN-THE SOFTWARE.+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
− benchmarks/parse.hs
@@ -1,54 +0,0 @@-{-# OPTIONS_GHC -fno-warn-orphans #-}-{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}-{-# LANGUAGE OverloadedStrings #-}--import Criterion.Main-import Text.Damn.Packet-import Data.Maybe (fromJust)-import Data.Monoid-import Data.Text (Text)--main :: IO ()-main = defaultMain [- bgroup "Parsing" [- bench "phony" $ nf parse' ""- , bench "cmd" $ nf parse' cmd- , bench "cmdParam" $ nf parse' cmdParam- , bench "cmdArgs" $ nf parse' cmdArgs- , bench "cmdParamArgs" $ nf parse' cmdParamArgs- , bench "cmdBody" $ nf parse' cmdBody- , bench "cmdParamBody" $ nf parse' cmdParamBody- , bench "cmdArgsBody" $ nf parse' cmdArgsBody- , bench "cmdParamArgsBody" $ nf parse' cmdParamArgsBody ]- , bgroup "Rendering" [- bench "cmd" $ nf render cmdPkt- , bench "cmdParam" $ nf render cmdParamPkt- , bench "cmdArgs" $ nf render cmdArgsPkt- , bench "cmdParamArgs" $ nf render cmdParamArgsPkt- , bench "cmdBody" $ nf render cmdBodyPkt- , bench "cmdParamBody" $ nf render cmdParamBodyPkt- , bench "cmdArgsBody" $ nf render cmdArgsBodyPkt- , bench "cmdParamArgsBody" $ nf render cmdParamArgsBodyPkt ]- ]--cmd, cmdParam, cmdArgs, cmdParamArgs,- cmdBody, cmdParamBody, cmdArgsBody, cmdParamArgsBody :: Text-cmd = "ping"-cmdParam = "ping foobar"-cmdArgs = "ping\nfoo=bar\nbaz=qux"-cmdParamArgs = "ping foobar\nfoo=bar\nbaz=qux"-cmdBody = cmd <> "\n\nfoobar"-cmdParamBody = cmdParam <> "\n\nfoobar"-cmdArgsBody = cmdArgs <> "\n\nfoobar"-cmdParamArgsBody = cmdParamArgs <> "\n\nfoobar"--cmdPkt, cmdParamPkt, cmdArgsPkt, cmdParamArgsPkt, cmdBodyPkt,- cmdParamBodyPkt, cmdArgsBodyPkt, cmdParamArgsBodyPkt :: Packet-cmdPkt = fromJust $! parse' cmd-cmdParamPkt = fromJust $! parse' cmdParam-cmdArgsPkt = fromJust $! parse' cmdArgs-cmdParamArgsPkt = fromJust $! parse' cmdParamArgs-cmdBodyPkt = fromJust $! parse' cmdBody-cmdParamBodyPkt = fromJust $! parse' cmdParamBody-cmdArgsBodyPkt = fromJust $! parse' cmdArgsBody-cmdParamArgsBodyPkt = fromJust $! parse' cmdParamArgsBody
damnpacket.cabal view
@@ -1,48 +1,43 @@ name: damnpacket-version: 0.6.0-synopsis: Parsing dAmn packets+version: 1.0.0+synopsis: Parsing dAmn messages+description: This module provides a datatype and convenience functions for parsing, manipulating, and rendering deviantART Message Network messages. license: MIT license-file: LICENSE-homepage: https://github.com/joelteon/damnpacket-author: Joel Taylor-maintainer: me@joelt.io-category: Text+author: Jude Taylor+maintainer: me@jude.bio+-- copyright: +category: Network build-type: Simple+extra-source-files: ChangeLog.md cabal-version: >=1.10 library- exposed-modules: Text.Damn.Packet- other-modules: Text.Damn.Packet.Internal- Text.Damn.Packet.Parser- build-depends: base >= 4.4 && < 4.8- , attoparsec >= 0.11- , bytestring >= 0.10- , containers >= 0.4- , deepseq >= 1.3- , text >= 1.1- hs-source-dirs: src- default-language: Haskell2010+ exposed-modules: Network.Damn+ Network.Damn.Format.IRC+ Network.Damn.Format.Damn+ other-modules: Network.Damn.Tablumps+ Network.Damn.Tablumps.TH+ Network.Damn.Format.Base+ Network.Damn.Format.Damn.Internal+ -- other-extensions: + build-depends: base == 4.*+ , attoparsec+ , bytestring+ , fail+ , html-entities+ , template-haskell+ , text+ hs-source-dirs: src+ default-language: Haskell2010+ ghc-options: -Wall -test-suite render- type: exitcode-stdio-1.0- main-is: render.hs- ghc-options: -w -threaded -rtsopts -with-rtsopts=-N- hs-source-dirs: tests- build-depends: base- , containers >= 0.5- , damnpacket- , QuickCheck >= 2.6- , text >= 1.1- default-language: Haskell2010+test-suite parse+ main-is: parse.hs+ hs-source-dirs: test+ type: exitcode-stdio-1.0+ build-depends: base, bytestring, damnpacket, HUnit, hspec, QuickCheck -benchmark parse- type: exitcode-stdio-1.0- main-is: parse.hs- ghc-options: -Wall -O2 -threaded- hs-source-dirs: benchmarks- build-depends: base- , containers >= 0.5- , criterion >= 0.8- , damnpacket- , text >= 1.1- default-language: Haskell2010+source-repository head+ location: https://github.com/pikajude/damnpacket+ type: git
+ src/Network/Damn.hs view
@@ -0,0 +1,226 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ViewPatterns #-}++-- | This module provides a datatype and convenience functions for parsing,+-- manipulating, and rendering deviantART Message Network messages.+module Network.Damn (+ -- *** Datatypes+ Message(..),+ SubMessage(..),+ MessageBody,+ -- *** Working with message bodies+ bodyBytes, Formatter, bodyWithFormat,+ toBody, toBodyText,+ -- *** Working with sub-messages+ subMessage,+ pattern SubM,+ -- *** Parsing+ parseMessage,+ messageP,+ -- *** Rendering+ render,+ -- *** Tablumps+ Lump(..)+) where++import Control.Applicative+import qualified Control.Monad+import Control.Monad.Fail+import Data.Attoparsec.ByteString hiding (word8)+import qualified Data.Attoparsec.ByteString as A+import qualified Data.Attoparsec.ByteString.Char8 as C+import Data.ByteString+import Data.Char+import Data.Ix+import Data.Monoid+import Data.String+import Data.Text hiding (singleton)+import Data.Word+import Network.Damn.Format.Base (Formatter)+import Network.Damn.Format.Damn.Internal (textToBytes)+import Network.Damn.Tablumps+import Prelude hiding (fail)++-- | A top-level dAmn message.+--+-- General syntax for a message:+--+-- @+-- name arg+-- attr1=val1+-- attr2=val2+--+-- body+-- @+--+-- As reflected in the field types of 'Message', the 'arg' and 'body' are+-- both optional.+--+-- Attribute values are considered to be textual data and generally consist+-- of part reasons, privclass names, users' "taglines" and so on. The+-- message body can either be treated as text or as a 'SubMessage' (see+-- 'MessageBody').+--+-- Note that dAmn is a primarily browser-based platform; it deals with+-- only ISO-8859-1 output and input and inserts chat messages directly into+-- the DOM. As a consequence, correctly displaying characters past the ASCII+-- block requires the use of HTML entities.+--+-- All functions in this module transparently convert HTML entities+-- embedded in 'ByteString's to 'Text' (and back; see 'toBodyText'). Thus,+-- when 'Text' appears in fields of this record or of 'SubMessage', you can+-- assume that the HTML entity decoding step has already been handled.+data Message = Message+ { messageName :: ByteString+ , messageArgument :: Maybe ByteString+ , messageAttrs :: [(ByteString, Text)]+ , messageBody :: Maybe MessageBody+ } deriving (Eq, Show)++-- | A second-level dAmn message. Note that this message can omit the+-- name/argument pair.+data SubMessage = SubMessage+ { subMessageName :: Maybe ByteString+ , subMessageArgument :: Maybe ByteString+ , subMessageAttrs :: [(ByteString, Text)]+ , subMessageBody :: Maybe MessageBody+ } deriving (Eq, Show)++-- | The body of a message, which can be converted to various formats+-- ('bodyWithFormat') or parsed as a 'SubMessage' ('subMessage').+data MessageBody = MessageBody+ { -- | View the original binary content of a 'MessageBody'.+ --+ -- To interpret this as textual data, use+ -- 'bodyWithFormat'.+ bodyBytes :: ByteString+ -- | Try to parse a 'MessageBody' as a 'SubMessage'.+ , subMessage :: forall m. MonadFail m => m SubMessage+ }++instance IsString MessageBody where+ fromString = toBody . fromString++-- bodyRaw (MessageBody b _) = show b+-- bodyText (MessageBody b _) = lumpsToText b+-- bodyTextInline (MessageBody b _) = lumpsToTextInline b++instance Show MessageBody where+ show (MessageBody b _) = show b++instance Eq MessageBody where+ MessageBody b _ == MessageBody b1 _ = b == b1++-- | 'subMessage' as a pattern.+--+-- @+-- case messageBody of+-- Sub (SubMessage name args attrs body)) -> ...+-- _ -> error "No parse"+-- @+--+-- Can be nested:+--+-- @+-- isJoinPacket :: Message -> Bool+-- isJoinPacket (Message "recv" room _+-- (Sub (SubMessage (Just "join") (Just uname) _+-- (Sub (SubMessage Nothing Nothing userAttrs _)))))+-- = True+-- isJoinPacket _ = False+-- @+pattern SubM :: SubMessage -> Maybe MessageBody+pattern SubM pkt <- ((>>= subMessage) -> Just pkt)++-- | Convert a 'MessageBody' to some stringlike representation using the+-- given 'Formatter'. (See 'Network.Damn.Format.Damn.damnFormat').+bodyWithFormat :: Monoid s => Formatter s -> MessageBody -> s+bodyWithFormat f = foldMap f . toLumps . bodyBytes++messageP :: Parser Message+messageP = do+ name <- C.takeWhile1 C.isAlpha_iso8859_15+ next <- C.peekChar'+ arg <- if next == ' '+ then C.char ' ' *> (Just <$> C.takeWhile1 (/= '\n'))+ else pure Nothing++ _ <- C.char '\n'+ attrs <- many attr++ body <- parseBody++ return $ Message name arg attrs body++parseBody :: Parser (Maybe MessageBody)+parseBody = do+ next <- C.anyChar+ case next of+ '\n' -> Just . toBody+ <$> Data.Attoparsec.ByteString.takeWhile (/= 0) <* A.word8 0+ '\0' -> pure Nothing+ _ -> Control.Monad.fail "Malformed packet"++subMessageP :: Parser SubMessage+subMessageP = do+ firstAttr <- optional attr+ case firstAttr of+ Just a -> do+ otherAttrs <- many attr+ body <- parseBody+ return $ SubMessage Nothing Nothing (a:otherAttrs) body+ Nothing -> do+ Message a b c d <- messageP+ return $ SubMessage (Just a) b c d++attr :: Parser (ByteString, Text)+attr = do+ k <- takeWhile1 nameChars+ _ <- C.char '='+ v <- C.takeWhile (/= '\n')+ _ <- C.char '\n'+ return (k, bytesToText v)++nameChars :: Word8 -> Bool+nameChars x = inRange (integralOrd 'a', integralOrd 'z') x+ || inRange (integralOrd 'A', integralOrd 'Z') x+ || inRange (integralOrd '0', integralOrd '9') x+ where integralOrd = fromIntegral . ord++-- | 'MessageBody' smart constructor.+toBody :: ByteString -> MessageBody+toBody x = MessageBody x+ (either Control.Monad.fail return $ parseOnly subMessageP (x <> "\0"))++-- | Like 'toBody', but convert codepoints outside the ASCII range to HTML+-- entities.+--+-- Note that this is NOT equivalent to @toBody . encodeUtf8@.+toBodyText :: Text -> MessageBody+toBodyText = toBody . textToBytes++-- | @'parseOnly' 'messageP'@+parseMessage :: ByteString -> Either String Message+parseMessage = parseOnly messageP++-- | Convert a 'Message' back into a 'ByteString' to send to dAmn. The null+-- byte is appended. In addition, all characters outside the ASCII block+-- are converted to HTML entities, thus+--+-- >>> render (Message "foo" (Just "bar") [("attr1", "☭")] Nothing)+-- "foo bar\nattr1=☭\n\NUL"+render :: Message -> ByteString+render (Message name arg attrs body) = appendArg arg name+ <> "\n"+ <> renderAttrs attrs+ <> renderBody body+ <> "\0"+ where+ appendArg (Just b) = (<> (" " <> b))+ appendArg _ = id+ renderAttrs [] = ""+ renderAttrs ((a,b):bs) = a <> "=" <> textToBytes b <> "\n" <> renderAttrs bs+ renderBody (Just (MessageBody b _)) = "\n" <> b+ renderBody _ = ""
+ src/Network/Damn/Format/Base.hs view
@@ -0,0 +1,10 @@+module Network.Damn.Format.Base (+ module Network.Damn.Format.Base,+ module Network.Damn.Tablumps,+ Text+) where++import Data.Text+import Network.Damn.Tablumps++type Formatter m = Either Text Lump -> m
+ src/Network/Damn/Format/Damn.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE OverloadedStrings #-}++module Network.Damn.Format.Damn (+ damnFormat,+ damnFormatBytes+) where++import Data.ByteString (ByteString)+import Data.Text.Encoding+import Network.Damn.Format.Base+import Network.Damn.Format.Damn.Internal++-- | @damnFormat@ is, essentially, a formatter that transforms a raw+-- dAmn message into the text that the sender typed into the dAmn web+-- client.+damnFormat :: Formatter Text+damnFormat = either id (decodeUtf8 . damnFormat')++-- | @encodeUtf8 . damnFormat@+--+-- This function is defined separately to avoid an encoding round-trip.+damnFormatBytes :: Formatter ByteString+damnFormatBytes = either encodeUtf8 damnFormat'
+ src/Network/Damn/Format/Damn/Internal.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE OverloadedStrings #-}++module Network.Damn.Format.Damn.Internal (+ textToBytes, damnFormat'+) where++import Data.ByteString (ByteString)+import Data.ByteString.Builder+import qualified Data.ByteString.Lazy as LB (toStrict)+import Data.Char+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text+import Network.Damn.Tablumps.TH++textToBytes :: Text -> ByteString+textToBytes = LB.toStrict . toLazyByteString . Data.Text.foldr (\ c b -> maybeEscape c <> b) "" where+ maybeEscape c+ | ord c <= 127 = word8 (fromIntegral $ ord c)+ | otherwise = "&#x" <> word32Hex (fromIntegral $ ord c) <> ";"++damnFormat' :: Lump -> ByteString+damnFormat' (A x y) = "<a href=\"" <> x <> "\" title=\"" <> y <> "\">"+damnFormat' C_A = "</a>"+damnFormat' (Abbr x) = "<abbr title=\"" <> x <> "\">"+damnFormat' C_Abbr = "</abbr>"+damnFormat' (Acro x) = "<acronym title=\"" <> x <> "\">"+damnFormat' C_Acro = "</acronym>"+damnFormat' (Avatar x _) = ":icon" <> x <> ":"+damnFormat' B = "<b>"+damnFormat' C_B = "</b>"+damnFormat' Bcode = "<bcode>"+damnFormat' C_Bcode = "</bcode>"+damnFormat' Br = "<br/>"+damnFormat' Code = "<code>"+damnFormat' C_Code = "</code>"+damnFormat' (Dev _ x) = ":dev" <> x <> ":"+damnFormat' (Embed x y z) = "<embed src=\"" <> x <> "\" height=\"" <> y <> "\" width=\"" <> z <> "\">"+damnFormat' C_Embed = "</embed>"+damnFormat' (Emote x _ _ _ _) = x+damnFormat' I = "<i>"+damnFormat' C_I = "</i>"+damnFormat' (Iframe x y z) = "<iframe src=\"" <> x <> "\" height=\"" <> y <> "\" width=\"" <> z <> "\">"+damnFormat' C_Iframe = "</iframe>"+damnFormat' (Img x y z) = "<img src=\"" <> x <> "\" height=\"" <> y <> "\" width=\"" <> z <> "\">"+damnFormat' Li = "<li>"+damnFormat' C_Li = "</li>"+damnFormat' (Link x _) = x+damnFormat' Ol = "<ol>"+damnFormat' C_Ol = "</ol>"+damnFormat' P = "<p>"+damnFormat' C_P = "</p>"+damnFormat' S = "<s>"+damnFormat' C_S = "</s>"+damnFormat' Sub = "<sub>"+damnFormat' C_Sub = "</sub>"+damnFormat' Sup = "<sup>"+damnFormat' C_Sup = "</sup>"+damnFormat' (Thumb x _ _ _ _ _) = ":thumb" <> x <> ":"+damnFormat' U = "<u>"+damnFormat' C_U = "</u>"+damnFormat' Ul = "<ul>"+damnFormat' C_Ul = "</ul>"
+ src/Network/Damn/Format/IRC.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}++-- | Format a dAmn message body as an inline text approximation, including+-- IRC styles.+module Network.Damn.Format.IRC (+ ircFormat, Lines, unLines+) where++import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import Data.String+import Data.Text.Encoding+import Network.Damn.Format.Base+import Network.Damn.Format.Damn.Internal++newtype Lines = Lines ByteString deriving (Show, Monoid, IsString)++unLines :: Lines -> [ByteString]+unLines (Lines bs) = B.split 10 bs++-- | This formatter functions as 'Network.Damn.Format.Damn.damnFormat',+-- except that bold, italics, and underlines will be converted to mIRC+-- colors.+--+-- Additionally, @Text@ will be encoded as UTF-8.+--+-- Note that this formatter will generate 'Lines', as newlines (to which+-- @&br\\t@ translates) cannot appear in IRC messages.+ircFormat :: Formatter Lines+ircFormat = either (Lines . encodeUtf8) (Lines . ircFormat')++ircFormat' :: Lump -> ByteString+ircFormat' B = "\x02"+ircFormat' C_B = "\x02"+ircFormat' Br = "\n"+ircFormat' I = "\x1D"+ircFormat' C_I = "\x1D"+ircFormat' U = "\x1F"+ircFormat' C_U = "\x1F"+ircFormat' x = damnFormat' x
+ src/Network/Damn/Tablumps.hs view
@@ -0,0 +1,129 @@+{-# OPTIONS_GHC -ddump-splices #-}+{-# LANGUAGE NoMonomorphismRestriction #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Network.Damn.Tablumps (+ module Network.Damn.Tablumps,+ Lump(..)+) where++import Control.Applicative+import Control.Arrow (left)+import Data.Attoparsec.ByteString hiding (word8)+import qualified Data.Attoparsec.ByteString.Char8 as C+import Data.ByteString (ByteString)+import Data.Monoid+import Data.Text (Text)+import Data.Text.Encoding+import Data.Text.Internal.Builder (toLazyText)+import Data.Text.Lazy (toStrict)+import HTMLEntities.Decoder+import Network.Damn.Tablumps.TH++-- type Lump = (ByteString, [ByteString])+-- data Lump = Lump deriving (Eq, Show)++tablumpP :: Parser [Either ByteString Lump]+tablumpP = many $ (Left <$> (C.takeWhile1 (/= '&')))+ <|> lump+ <|> fmap Left (C.string "&")++lump :: Parser (Either a Lump)+lump = (C.char '&' *>) $ choice+ [ $(ary 2 "a" 'A)+ , $(ary 0 "/a" 'C_A)++ , $(ary 1 "abbr" 'Abbr)+ , $(ary 0 "/abbr" 'C_Abbr)++ , $(ary 1 "acro" 'Acro)+ , $(ary 0 "/acro" 'C_Acro)++ , $(ary 2 "avatar" 'Avatar)++ , $(ary 0 "b" 'B)+ , $(ary 0 "/b" 'C_B)++ , $(ary 0 "bcode" 'Bcode)+ , $(ary 0 "/bcode" 'C_Bcode)++ , $(ary 0 "br" 'Br)++ , $(ary 0 "code" 'Code)+ , $(ary 0 "/code" 'C_Code)++ , $(ary 2 "dev" 'Dev)++ , $(ary 3 "embed" 'Embed)+ , $(ary 0 "/embed" 'C_Embed)++ , $(ary 5 "emote" 'Emote)++ , $(ary 0 "i" 'I)+ , $(ary 0 "/i" 'C_I)++ , $(ary 3 "iframe" 'Iframe)+ , $(ary 0 "/iframe" 'C_Iframe)++ , $(ary 3 "img" 'Img)++ , $(ary 0 "li" 'Li)+ , $(ary 0 "/li" 'C_Li)++ , link++ , $(ary 0 "ol" 'Ol)+ , $(ary 0 "/ol" 'C_Ol)++ , $(ary 0 "p" 'P)+ , $(ary 0 "/p" 'C_P)++ , $(ary 0 "s" 'S)+ , $(ary 0 "/s" 'C_S)++ , $(ary 0 "sub" 'Sub)+ , $(ary 0 "/sub" 'C_Sub)++ , $(ary 0 "sup" 'Sup)+ , $(ary 0 "/sup" 'C_Sup)++ , $(ary 6 "thumb" 'Thumb)++ , $(ary 0 "u" 'U)+ , $(ary 0 "/u" 'C_U)++ , $(ary 0 "ul" 'Ul)+ , $(ary 0 "/ul" 'C_Ul)+ ]+ where+ -- ary n s = do+ -- string s+ -- C.char '\t'+ -- fmap (Right . (,) s) $ count n arg+ link = do+ _ <- string "link"+ _ <- C.char '\t'+ arg1 <- arg+ arg2 <- arg+ case arg2 of+ "&" -> pure $ Right $ Link arg1 Nothing+ _ -> do+ _ <- string "&\t"+ pure $ Right $ Link arg1 (Just arg2)+ arg = C.takeWhile (/= '\t') <* C.char '\t'++toLumps :: ByteString -> [Either Text Lump]+toLumps t = case parseOnly tablumpP t of+ Right y -> map (left (htmlDecode . bytesToText)) $ joinLefts y+ Left _ -> [Left $ bytesToText t]+ where+ joinLefts (Left a : Left b : xs) = joinLefts (Left (a <> b) : xs)+ joinLefts (x:xs) = x : joinLefts xs+ joinLefts [] = []++bytesToText :: ByteString -> Text+bytesToText = decodeLatin1++htmlDecode :: Text -> Text+htmlDecode = toStrict . toLazyText . htmlEncodedText
+ src/Network/Damn/Tablumps/TH.hs view
@@ -0,0 +1,74 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveLift #-}+{-# LANGUAGE TemplateHaskell #-}++#define ARG ByteString++module Network.Damn.Tablumps.TH where++import qualified Data.Attoparsec.ByteString.Char8 as C+import Data.ByteString (ByteString, unpack)+import Language.Haskell.TH+import Language.Haskell.TH.Syntax++ary :: Int -> String -> Name -> ExpQ+ary n s con = [e|do+ _ <- string s+ _ <- C.char '\t'+ Right <$> $(mkApps)+ |]+ where+ mkApps = foldl (\ a b -> [e|$(a) <*> $(b)|]) [e|pure $(conE con)|]+ $ replicate n [e|C.takeWhile (/= '\t') <* C.char '\t'|]++-- | Tokens representing tablumps.+--+-- These constructors are defined first in order of arity, then+-- alphabetically.+data Lump = A ARG ARG+ | C_A+ | Abbr ARG+ | C_Abbr+ | Acro ARG+ | C_Acro+ | Avatar ARG ARG+ | B+ | C_B+ | Bcode+ | C_Bcode+ | Br+ | Code+ | C_Code+ | Dev ARG ARG+ | Embed ARG ARG ARG+ | C_Embed+ | Emote ARG ARG ARG ARG ARG+ | I+ | C_I+ | Iframe ARG ARG ARG+ | C_Iframe+ | Img ARG ARG ARG+ | Li+ | C_Li+ | Link ARG (Maybe ARG)+ | Ol+ | C_Ol+ | P+ | C_P+ | S+ | C_S+ | Sub+ | C_Sub+ | Sup+ | C_Sup+ | Thumb ARG ARG ARG ARG ARG ARG+ | U+ | C_U+ | Ul+ | C_Ul+ deriving (Lift, Eq, Show)++instance Lift ByteString where+ lift b = [e|pack ws|]+ where ws = unpack b
− src/Text/Damn/Packet.hs
@@ -1,27 +0,0 @@--- | Provides convenience functions for manipulating dAmn packets.-module Text.Damn.Packet (- -- * The Packet datatype- Packet(..),- Arguments,-- -- ** Subpackets- pktSubpacket,- pktSubpacket',-- -- ** Packet lenses- pktCommandL,- pktParameterL,- pktArgsL,- pktBodyL,- pktSubpacketL,-- -- * Parsing- parse,- parse',-- -- * Rendering packets- render-) where--import Text.Damn.Packet.Parser-import Text.Damn.Packet.Internal
− src/Text/Damn/Packet/Internal.hs
@@ -1,94 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE RankNTypes #-}---- | Internal functions for Packets.-module Text.Damn.Packet.Internal (- Packet(..),- Arguments,- pktCommandL,- pktParameterL,- pktArgsL,- pktBodyL-) where--import Control.Applicative-import Control.DeepSeq-import Data.Data-import Data.Map (Map)-import Data.Text (Text)---- | A type synonym--because pressing spacebar is pretty irritating.-type Arguments = Map Text Text--lens :: Functor f => (s -> a) -> (s -> b -> t) -> (a -> f b) -> s -> f t-lens sa sbt afb s = sbt s <$> afb (sa s)-{-# INLINE lens #-}---- | Represents a dAmn packet.------ Packets are comprised of a command, which is mandatory, and three other--- optional elements: a \"parameter\", an argument list, and a body. Any--- combination of the latter three (or none of them) is valid, so the--- parser is fairly lenient.------ A packet with all four elements will look something like this:------ @---property chat:SomeChatroom---p=propertyName---by=name of setter---ts=timestamp------value of property--- @------ Parsing this results in the packet:------ @---'Packet' { 'pktCommand' = \"property\"--- , 'pktParameter' = 'Just' \"chat:SomeChatroom\"--- , 'pktArgs' = 'fromList' [(\"p\",\"propertyName\"),(\"by\",\"name of setter\"),(\"ts\",\"timestamp\")]--- , 'pktBody' = 'Just' \"value of property\"--- }--- @-data Packet = Packet { pktCommand :: Text- , pktParameter :: Maybe Text- , pktArgs :: Arguments- , pktBody :: Maybe Text- } deriving (Eq, Data, Show, Typeable)--instance NFData Packet where- rnf (Packet a b c d) = rnf a `seq`- rnf b `seq`- rnf c `seq`- rnf d `seq`- ()---- | A lens on 'pktCommand'.-pktCommandL :: Functor f- => (Text -> f Text)- -> Packet -> f Packet-pktCommandL = lens pktCommand (\pk b -> pk { pktCommand = b })-{-# INLINE pktCommandL #-}---- | A lens on 'pktParameter'.-pktParameterL :: Functor f- => (Maybe Text -> f (Maybe Text))- -> Packet -> f Packet-pktParameterL = lens pktParameter (\pk b -> pk { pktParameter = b })-{-# INLINE pktParameterL #-}---- | A lens on 'pktArgs'.-pktArgsL :: Functor f- => (Arguments -> f Arguments)- -> Packet -> f Packet-pktArgsL = lens pktArgs (\pk b -> pk { pktArgs = b })-{-# INLINE pktArgsL #-}---- | A lens on 'pktBody'.-pktBodyL :: Functor f- => (Maybe Text -> f (Maybe Text))- -> Packet -> f Packet-pktBodyL = lens pktBody (\pk b -> pk { pktBody = b })-{-# INLINE pktBodyL #-}
− src/Text/Damn/Packet/Parser.hs
@@ -1,112 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}---- | Parsing Packets.-module Text.Damn.Packet.Parser (- Packet(..),- parse, parse',- render,- pktSubpacket,- pktSubpacket',- pktSubpacketL-) where--import Prelude hiding (null)-import Control.Applicative ((<$>), (*>), liftA3, pure)-import Control.Arrow (second)-import qualified Data.Attoparsec.Text as A-import Data.Char-import qualified Data.Map as M-import Data.Monoid-import qualified Data.Text as T-import Text.Damn.Packet.Internal---- | A lens on 'pktSubpacket''.-pktSubpacketL :: Functor f => (Maybe Packet -> f (Maybe Packet)) -> Packet -> f Packet-pktSubpacketL afb s = setter s <$> afb (getter s)- where getter = pktSubpacket'- setter pkt m = pkt { pktBody = render <$> m }-{-# INLINE pktSubpacketL #-}---- | Due to the way dAmn packets are designed, it's not possible to--- unambiguously determine whether a packet has a subpacket or just a body.--- Thus you will need to request a subpacket yourself.-pktSubpacket :: Packet -> Either String Packet-pktSubpacket Packet { pktBody = b } =- case b of Nothing -> Left "Parent packet has no body!"- Just pk -> parse pk-{-# INLINE pktSubpacket #-}---- | Use when you don't care about the reason for parse failure.-pktSubpacket' :: Packet -> Maybe Packet-pktSubpacket' p = case pktSubpacket p of- Right pk -> Just pk- _ -> Nothing-{-# INLINE pktSubpacket' #-}---- | 'render' converts a packet back into the dAmn text format.--- This is used by 'pktSubpacketL' to fulfill the lens laws, but you might--- find it useful if you want to write packets to dAmn.-render :: Packet -> T.Text-render (Packet cmd prm args b) =- cmd- <> maybe "" (" " <>) prm- <> M.foldrWithKey- (\k v m -> "\n" <> k <> "=" <> v <> m) "" args- <> maybe "" ("\n\n" <>) b-{-# INLINE render #-}---- | Parse some text, providing a packet or the reason for parse failure.-parse :: T.Text -> Either String Packet-parse str = if T.null body- then packet- else addBody packet- where adjustedStr = accountForLoginSpace str- (header, body) = second (T.drop 2) $ T.breakOn "\n\n" adjustedStr- packet = A.parseOnly headP header- addBody (Right s) = Right $ s { pktBody = Just body }- addBody l = l---- | Parse some text, discarding any failure message.-parse' :: T.Text -> Maybe Packet-parse' s = case parse s of- Right pk -> Just pk- _ -> Nothing-{-# INLINE parse' #-}--headP :: A.Parser Packet-headP = do- cmd <- A.takeWhile1 (not . isSpace)- prm <- A.option Nothing paramP- args <- argsP- return $ Packet cmd prm args Nothing- where- paramP = fmap Just (A.char ' ' *> A.takeWhile1 (not . isSpace)) A.<?> "parameter"- argsP = do- ch <- A.option Nothing (Just <$> A.char '\n')- case ch of- Just '\n' -> liftA3 M.insert- (A.takeWhile1 (/= '='))- (A.char '=' >> A.takeWhile (/= '\n'))- argsP- _ -> pure mempty---- | The login packet looks like this:------ @---login username---e=event------symbol=~---realname=Some Name--- @------ That is, with an extra space after the \"event\" argument. Since this is--- unintuitive behavior and no other packet behaves this way, the parser has--- a special case for login packets where it will eliminate the extra newline.-accountForLoginSpace :: T.Text -> T.Text-accountForLoginSpace s = if "login " `T.isPrefixOf` s- then T.replace "\n\n" "\n" s- else s-{-# INLINE accountForLoginSpace #-}
+ test/parse.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}++import qualified Data.ByteString.Char8 as B8+import Data.String+import Network.Damn+import Test.Hspec+import Test.HUnit+import Test.QuickCheck++main = hspec $+ describe "packets" $ do+ it "basic parsing" $+ "dAmnServer 0.3\n\0" @?= Just (Message "dAmnServer" (Just "0.3") [] Nothing)++ it "attrs" $+ "a\na=b\nc=d\n\0" @?= Just (Message "a" Nothing [("a","b"),("c","d")] Nothing)++ it "subpacket" $+ (subMessage =<< messageBody =<< "a\n\nb c\nd=e\n\0")+ @?= Just (SubMessage (Just "b") (Just "c") [("d","e")] Nothing)++instance IsString (Maybe Message) where+ fromString x = case parseMessage $ B8.pack x of+ Left _ -> Nothing+ Right x -> Just x
− tests/render.hs
@@ -1,36 +0,0 @@-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# OPTIONS_GHC -fno-warn-orphans #-}--import Control.Applicative ((<*>), (<$>))-import Control.Monad (unless)-import Data.Map (fromList, toList)-import Data.Text (Text, pack, unpack)-import System.Exit-import Test.QuickCheck-import Test.QuickCheck.Test (isSuccess)-import Text.Damn.Packet--instance Arbitrary Text where- arbitrary = pack <$> vectorOf 10 (elements ['a'..'Z'])- shrink m = pack <$> shrink (unpack m)--instance Arbitrary Packet where- arbitrary = do- cmd <- arbitrary- prm <- arbitrary- args <- fromList <$> arbitrary- b <- arbitrary- return $ Packet cmd prm args b- shrink (Packet a b c d) = Packet <$> shrink a- <*> shrink b- <*> (fromList <$> shrink (toList c))- <*> shrink d--quickCheckExit :: Testable prop => prop -> IO ()-quickCheckExit prop = do- res <- quickCheckResult prop- unless (isSuccess res) exitFailure--main :: IO ()-main = quickCheckExit (\pk -> parse' (render pk) == Just pk)