Update data format + use Text
Getty Ritter
7 years ago
| 1 | 1 | {-# LANGUAGE LambdaCase #-} |
| 2 | 2 | |
| 3 |
module Data.TeLML.Parser (Fragment(..), |
|
| 3 | module Data.TeLML.Parser (Fragment(..), Tag(..), Document, parse) where | |
| 4 | 4 | |
| 5 | 5 | import Data.Char (isAlpha, isAlphaNum, isSpace) |
| 6 | 6 | import Data.TeLML.Type |
| 7 | ||
| 8 | import qualified Data.Text as T | |
| 7 | 9 | |
| 8 | 10 | type Result a = Either String (String, a) |
| 9 | 11 | type Parse a = String -> Result a |
| 42 | 44 | -- Parse a text fragment, handling escapes. This will end as soon as it |
| 43 | 45 | -- sees any non-escaped special character. |
| 44 | 46 | pText :: Parse Fragment |
| 45 |
pText = over |
|
| 47 | pText = over (TextFrag . T.pack) . go | |
| 46 | 48 | where go ('\\':x:xs) |
| 47 | 49 | | isSpecial x = (x:) `over` go xs |
| 48 | 50 | go i@(x:xs) |
| 78 | 80 | pTag i = |
| 79 | 81 | bind (pTagName i) $ \ (i', name) -> |
| 80 | 82 | bind (skipSpace i') $ \case |
| 81 |
('{':i'', ()) -> Tag |
|
| 83 | ('{':i'', ()) -> TagFrag `over` (Tag (T.pack name) `over` pArgs i'') | |
| 82 | 84 | ("",_) -> throw "unexpected end-of-document while parsing tag" |
| 83 | 85 | _ -> throw "expected start of block" |
| 84 | 86 | |
| 1 | 1 | {-# LANGUAGE DeriveDataTypeable #-} |
| 2 | 2 | |
| 3 |
module Data.TeLML.Type (Document, Fragment(..) |
|
| 3 | module Data.TeLML.Type (Document, Fragment(..), Tag(..)) where | |
| 4 | 4 | |
| 5 | import Control.DeepSeq (NFData(..)) | |
| 6 | import Data.Data (Data) | |
| 7 | import Data.Typeable (Typeable) | |
| 8 | import Data.String (IsString(..)) | |
| 5 | import Control.DeepSeq (NFData(..)) | |
| 6 | import Data.Data (Data) | |
| 7 | import qualified Data.Text as T | |
| 8 | import Data.Typeable (Typeable) | |
| 9 | import Data.String (IsString(..)) | |
| 9 | 10 | |
| 10 | 11 | -- | A 'Document' is zero or more 'Fragment's. |
| 11 | 12 | type Document = [Fragment] |
| 15 | 16 | -- constructor). The former is a raw string, and the latter consists |
| 16 | 17 | -- of a name followed by zero or more 'Document's. |
| 17 | 18 | data Fragment |
| 18 | = Text String | |
| 19 | | Tag String [Document] | |
| 19 | = TextFrag T.Text | |
| 20 | | TagFrag Tag | |
| 20 | 21 | deriving (Eq, Show, Typeable, Data) |
| 21 | 22 | |
| 23 | data Tag = Tag | |
| 24 | { tagName :: T.Text | |
| 25 | , tagPayload :: [Document] | |
| 26 | } deriving (Eq, Show, Typeable, Data) | |
| 27 | ||
| 22 | 28 | instance IsString Fragment where |
| 23 |
fromString = Text |
|
| 29 | fromString = TextFrag . fromString | |
| 24 | 30 | |
| 25 | 31 | instance NFData Fragment where |
| 26 | rnf (Text s) = rnf s | |
| 27 | rnf (Tag s l) = rnf s `seq` rnf l | |
| 32 | rnf (TextFrag s) = rnf s | |
| 33 | rnf (TagFrag t) = rnf t | |
| 34 | ||
| 35 | instance NFData Tag where | |
| 36 | rnf (Tag n l) = rnf n `seq` rnf l | |
| 1 |
module Data.TeLML(parse, Document, Fragment(..) |
|
| 1 | module Data.TeLML(parse, Document, Fragment(..), Tag(..)) where | |
| 2 | 2 | |
| 3 | 3 | import Data.TeLML.Parser |
| 16 | 16 | ghc-options: -Wall |
| 17 | 17 | other-modules: Data.TeLML.Parser, Data.TeLML.Type |
| 18 | 18 | build-depends: base >=4.7 && <5, |
| 19 |
deepseq >=1.4 && <2 |
|
| 19 | deepseq >=1.4 && <2, | |
| 20 | text | |
| 20 | 21 | default-language: Haskell2010 |
| 21 | 22 | |
| 22 | 23 | source-repository head |
| 5 | 5 | |
| 6 | 6 | import Control.Monad (void) |
| 7 | 7 | import Data.TeLML |
| 8 | import qualified Data.Text as T | |
| 8 | 9 | import Text.Blaze.Html |
| 9 | 10 | import Text.Blaze.Html5 hiding (map, head, html) |
| 10 | 11 | import Text.Blaze.Html5.Attributes hiding (name, span) |
| 12 | 13 | import Prelude hiding (div, span) |
| 13 | 14 | |
| 14 | 15 | -- | Render a TeLML document with an extra set of possible tags. |
| 15 |
renderWith :: [( |
|
| 16 | renderWith :: [(T.Text, Renderer)] -> Document -> Either String Html | |
| 16 | 17 | renderWith rs = |
| 17 | 18 | fmap (void . sequence) . mapM (renderPara (basicTags ++ rs)) . gatherPara |
| 18 | 19 | |
| 26 | 27 | gatherPara :: Document -> [Document] |
| 27 | 28 | gatherPara = reverse . map reverse . go [[]] |
| 28 | 29 | where go rs [] = rs |
| 29 | go (r:rs) (t@Tag {}:ts) = go ((t:r):rs) ts | |
| 30 | go (r:rs) (Text s:ts) = case splitString s of | |
| 30 | go (r:rs) (t@TagFrag {}:ts) = go ((t:r):rs) ts | |
| 31 | go (r:rs) (TextFrag s:ts) = case splitString s of | |
| 31 | 32 | [] -> go (r:rs) ts |
| 32 | [x] -> go ((Text x:r):rs) ts | |
| 33 | xs -> go (map ((:[]) . Text) (tail xs) ++ | |
| 34 |
|
|
| 33 | [x] -> go ((TextFrag x:r):rs) ts | |
| 34 | xs -> go (map ((:[]) . TextFrag) (tail xs) ++ | |
| 35 | ((TextFrag (head xs):r) : rs)) ts | |
| 35 | 36 | go _ _ = error "[unreachable]" |
| 36 | 37 | |
| 37 | 38 | -- Split a string at double-newlines. |
| 38 | splitString :: String -> [String] | |
| 39 | splitString = filter (/= "") . go | |
| 40 | where go ('\n':'\n':xs) = "\n":go xs | |
| 41 | go (x:xs) = let r:rs = go xs in ((x:r):rs) | |
| 42 |
|
|
| 39 | splitString :: T.Text -> [T.Text] | |
| 40 | splitString = T.splitOn "\n\n" | |
| 43 | 41 | |
| 44 | 42 | -- This is just to make type signatures shorter |
| 45 | 43 | type HtmlE = Either String Html |
| 47 | 45 | type Renderer = (Fragment -> HtmlE, [Document]) -> HtmlE |
| 48 | 46 | |
| 49 | 47 | -- The built-in set of tags (subject to change) |
| 50 |
basicTags :: [( |
|
| 48 | basicTags :: [(T.Text, Renderer)] | |
| 51 | 49 | basicTags = |
| 52 | 50 | [ simpleTag "em" em |
| 53 | 51 | , simpleTag "strong" strong |
| 68 | 66 | , ("br", \_ -> return br) |
| 69 | 67 | , ("comment", \_ -> return "") |
| 70 | 68 | , ("link" |
| 71 |
, \case (f,[[Text |
|
| 69 | , \case (f,[[TextFrag l],r]) -> let go h = a ! href (toValue l) $ h | |
| 72 | 70 | in fmap (go . sequence_) (mapM f r) |
| 73 | 71 | (_,[_,_]) -> Left "link target should be string" |
| 74 | 72 | _ -> Left "wrong arity for link/1" |
| 75 | 73 | ) |
| 76 | 74 | , ("img" |
| 77 |
, \case (_, [[Text |
|
| 75 | , \case (_, [[TextFrag l]]) -> return (img ! src (toValue l)) | |
| 78 | 76 | (_,[_]) -> Left "image target should be string" |
| 79 | 77 | _ -> Left "wrong arity for img/1" |
| 80 | 78 | ) |
| 81 | 79 | ] |
| 82 |
where simpleTag :: |
|
| 80 | where simpleTag :: T.Text -> (Html -> Html) -> (T.Text, Renderer) | |
| 83 | 81 | simpleTag name tag = |
| 84 | 82 | ( name |
| 85 | 83 | , \case (f,[rs]) -> fmap (tag . sequence_) (mapM f rs) |
| 86 |
_ -> Left ("wrong arity for " ++ |
|
| 84 | _ -> Left ("wrong arity for " ++ T.unpack name ++ "/1") | |
| 87 | 85 | ) |
| 88 | 86 | listTag name tag = |
| 89 | 87 | ( name |
| 91 | 89 | ) |
| 92 | 90 | |
| 93 | 91 | -- render a single paragraph |
| 94 |
renderPara :: [( |
|
| 92 | renderPara :: [(T.Text, Renderer)] -> Document -> Either String Html | |
| 95 | 93 | renderPara taglist ds = fmap (p . sequence_) (mapM go ds) |
| 96 | where go (Text ts) = Right (toMarkup ts) | |
| 97 | go (Tag tx rs) = exec tx rs taglist | |
| 94 | where go (TextFrag ts) = Right (toMarkup ts) | |
| 95 | go (TagFrag (Tag tx rs)) = exec tx rs taglist | |
| 98 | 96 | exec name args ((tag, func):tags) |
| 99 | 97 | | name == tag = case func (go, args) of |
| 100 | 98 | Right html -> Right html |
| 101 | 99 | Left {} -> exec name args tags |
| 102 | 100 | exec name args (_:tags) = exec name args tags |
| 103 | 101 | exec name args [] = Left $ |
| 104 |
"Error: no match for tag " ++ |
|
| 102 | "Error: no match for tag " ++ T.unpack name ++ "/" ++ show (length args) | |
| 20 | 20 | telml, |
| 21 | 21 | blaze-markup, |
| 22 | 22 | blaze-html, |
| 23 |
mtl |
|
| 23 | mtl, | |
| 24 | text | |
| 24 | 25 | |
| 25 | 26 | test-suite spec |
| 27 | default-language: Haskell2010 | |
| 26 | 28 | type: exitcode-stdio-1.0 |
| 27 | 29 | ghc-options: -Wall |
| 28 | 30 | hs-source-dirs: test |
| 29 | 31 | main-is: Spec.hs |
| 32 | other-modules: Telml.MarkupSpec | |
| 30 | 33 | build-depends: base == 4.* |
| 31 | 34 | , telml |
| 32 | 35 | , telml-markup |
| 33 | 36 | , hspec == 2.* |
| 34 | 37 | , blaze-markup |
| 38 | build-tools: hspec-discover | |
| 35 | 39 | |
| 36 | 40 | source-repository head |
| 37 | 41 | type: git |
| 22 | 22 | doRender "\\em{foo}" `shouldBe` Right "<p><em>foo</em></p>" |
| 23 | 23 | it "should embolden" $ do |
| 24 | 24 | doRender "\\strong{foo}" `shouldBe` Right "<p><strong>foo</strong></p>" |
| 25 |
it "should lis |
|
| 25 | it "should list" $ do | |
| 26 | 26 | doRender "\\ul{\\li{one}\\li{two}}" `shouldBe` |
| 27 | 27 | Right "<p><ul><li>one</li><li>two</li></ul></p>" |
| 13 | 13 | , arg |
| 14 | 14 | , both |
| 15 | 15 | ) where |
| 16 | ||
| 17 | import qualified Data.Text as T | |
| 16 | 18 | |
| 17 | 19 | import Data.TeLML |
| 18 | 20 | |
| 39 | 41 | Left err -> Left err |
| 40 | 42 | Right v -> runParse (f v) s |
| 41 | 43 | |
| 42 |
select :: |
|
| 44 | select :: T.Text -> Parse [Document] t -> Parse Document [t] | |
| 43 | 45 | select name content = Parse $ \ s -> each s |
| 44 | 46 | where |
| 45 | 47 | each [] = return [] |
| 46 |
each (Tag |
|
| 48 | each (TagFrag (Tag t doc):xs) | |
| 47 | 49 | | t == name = (:) <$> runParse content doc <*> each xs |
| 48 | 50 | each (_:xs) = each xs |
| 49 | 51 | |
| 50 |
field :: |
|
| 52 | field :: T.Text -> (Parse [Document] t) -> Parse Document t | |
| 51 | 53 | field name content = Parse $ \ s -> find s |
| 52 | 54 | where |
| 53 | find [] = Left ("Unable to find tag \\" ++ name) | |
| 54 | find (Tag t doc:_) | |
| 55 | find [] = Left ("Unable to find tag \\" ++ T.unpack name) | |
| 56 | find (TagFrag (Tag t doc):_) | |
| 55 | 57 | | t == name = runParse content doc |
| 56 | 58 | find (_:xs) = find xs |
| 57 | 59 | |
| 68 | 70 | [a, b] -> (,) <$> runParse l a <*> runParse r b |
| 69 | 71 | _ -> Left ("Wrong arity for `both`: " ++ show (length s)) |
| 70 | 72 | |
| 71 | text :: Parse Document String | |
| 72 | text = Parse (\ s -> concat <$> traverse go s) | |
| 73 | where go (Text str) = Right str | |
| 74 | go (Tag t _) = Left ("Expected Text fragment, found \\" ++ show t) | |
| 73 | text :: Parse Document T.Text | |
| 74 | text = Parse (\ s -> T.concat <$> traverse go s) | |
| 75 | where go (TextFrag str) = Right str | |
| 76 | go (TagFrag (Tag t _)) = Left ("Expected Text fragment, found \\" ++ T.unpack t) | |
| 75 | 77 | |
| 76 | 78 | document :: Parse Document Document |
| 77 | 79 | document = Parse (\ s -> Right s) |