Update data format + use Text
Getty Ritter
6 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) |