Drop GADT in favor of simpler typeclasses
Getty Ritter
6 years ago
44 | 44 | splitString :: T.Text -> [T.Text] |
45 | 45 | splitString = T.splitOn "\n\n" |
46 | 46 | |
47 | -- This is just to make type signatures shorter | |
48 | type HtmlE = Either String Html | |
49 | 47 | |
50 | type Renderer = (Fragment -> HtmlE, [Document]) -> HtmlE | |
51 | ||
52 | infixr 5 :=> | |
53 | data a :=> b = a :=> b deriving (Eq, Show) | |
54 | ||
48 | -- | The 'TagArguments' class allow us to define a new tag with a name | |
49 | -- and a simple function, and cuts out a lot of the boilerplate. | |
55 | 50 | class TagArguments t where |
56 | 51 | toType :: t -> [T.Text] |
57 |
taExec :: t |
|
52 | taExec :: t | |
53 | -> [Document] | |
54 | -> (Fragment -> Either String Html) | |
55 | -> Maybe (Either String Html) | |
58 | 56 | |
59 |
instance |
|
57 | instance TagArguments Html where | |
60 | 58 | toType _ = [] |
61 | taExec r [] go = Just (r ()) | |
62 | taExec r _ _ = Nothing | |
59 | taExec h [] _ = Just (Right h) | |
60 | taExec _ _ _ = Nothing | |
63 | 61 | |
64 | 62 | instance TagArguments r => TagArguments (Str -> r) where |
65 | 63 | toType _ = "_" : toType (undefined :: r) |
66 | 64 | taExec f ([TextFrag t]:rs) go = taExec (f (Str t)) rs go |
67 | 65 | taExec _ _ _ = Nothing |
68 | 66 | |
69 | instance TagArguments r => TagArguments (Doc -> r) where | |
70 | toType _ = "string" : toType (undefined :: r) | |
71 | taExec f (doc:rs) go = taExec (f (Doc doc)) rs go | |
72 | taExec _ [] _ = Nothing | |
67 | instance TagArguments r => TagArguments (Maybe Str -> r) where | |
68 | toType _ = "_" : toType (undefined :: r) | |
69 | taExec f ([TextFrag t]:rs) go = taExec (f (Just (Str t))) rs go | |
70 | taExec f [] go = taExec (f Nothing) [] go | |
71 | taExec _ _ _ = Nothing | |
73 | 72 | |
74 | 73 | instance TagArguments r => TagArguments (H -> r) where |
75 | 74 | toType _ = "_" : toType (undefined :: r) |
80 | 79 | Right h' -> taExec (f (H h')) rs go |
81 | 80 | taExec _ [] _ = Nothing |
82 | 81 | |
83 |
instance ( |
|
82 | instance (h ~ Html) => TagArguments (Hs -> h) where | |
84 | 83 | toType _ = ["..."] |
85 | 84 | taExec f docs go = |
86 | 85 | let h = fmap sequence_ (mapM go (concat docs)) |
87 | 86 | in case h of |
88 | 87 | Left err -> return (Left err) |
89 | Right h' -> return (f (Hs h')) | |
90 | taExec _ [] _ = Nothing | |
88 | Right h' -> return (Right (f (Hs h'))) | |
91 | 89 | |
92 |
data Tag |
|
90 | data TagDescription = forall t. TagArguments t => TagDescription | |
93 | 91 | { tgName :: T.Text |
94 | 92 | , tgFunc :: t |
95 | 93 | } |
96 | 94 | |
97 | newtype Str = Str T.Text deriving (Eq, Show) | |
98 | newtype Doc = Doc Document deriving (Eq, Show) | |
95 | -- | The 'Str' newtype will match a literal chunk of non-formatted, | |
96 | -- non-structured text. | |
97 | newtype Str = Str { fromStr :: T.Text } deriving (Eq, Show) | |
98 | ||
99 | -- | The 'H' newtype will match a single, pre-rendered argument | |
99 | 100 | newtype H = H Html |
101 | ||
102 | -- | The 'Hs' newtype will match a concatenated set of pre-rendered | |
103 | -- arguments | |
100 | 104 | newtype Hs = Hs Html |
101 | newtype Docs = Docs [Document] deriving (Eq, Show) | |
102 | 105 | |
103 | exec :: T.Text -> [Document] -> [Tag'] -> Either String Html | |
104 | exec name args (Tag' tag func:_) | |
106 | mkTag :: TagArguments t => T.Text -> t -> TagDescription | |
107 | mkTag = TagDescription | |
108 | ||
109 | exec :: T.Text -> [Document] -> [TagDescription] -> Either String Html | |
110 | exec name args (TagDescription tag func:_) | |
105 | 111 | | name == tag = case taExec func args undefined of |
106 | 112 | Nothing -> Left $ unwords [ "Tag" |
107 | 113 | , T.unpack ('\\' `T.cons` name) |
113 | 119 | exec name args (_:rs) = exec name args rs |
114 | 120 | exec name args [] = Left $ |
115 | 121 | "Error: no match for tag " ++ T.unpack name ++ "/" ++ show (length args) |
116 | ||
117 | data Args t where | |
118 | EndArg :: Args () | |
119 | ListArg :: Args [Document] | |
120 | FragmentArg :: Args rest -> Args (Document :=> rest) | |
121 | TextArg :: Args rest -> Args (T.Text :=> rest) | |
122 | ||
123 | matchArguments :: Args t -> [Document] -> Maybe t | |
124 | matchArguments EndArg [] = return () | |
125 | matchArguments EndArg _ = Nothing | |
126 | matchArguments ListArg ds = return ds | |
127 | matchArguments (FragmentArg rs) (d:ds) = | |
128 | (d :=>) `fmap` matchArguments rs ds | |
129 | matchArguments (FragmentArg EndArg) [] = return ([TextFrag ""] :=> ()) | |
130 | matchArguments (FragmentArg _) [] = Nothing | |
131 | matchArguments (TextArg rs) ([TextFrag t]:ds) = | |
132 | (t :=>) `fmap` matchArguments rs ds | |
133 | matchArguments (TextArg _) _ = Nothing | |
134 | ||
135 | argType :: Args t -> T.Text | |
136 | argType t = "{" `T.append` T.intercalate "|" (toType t) `T.append` "}" | |
137 | where | |
138 | toType :: Args t -> [T.Text] | |
139 | toType EndArg = [] | |
140 | toType ListArg = ["..."] | |
141 | toType (FragmentArg rs) = "_" : toType rs | |
142 | toType (TextArg rs) = "string" : toType rs | |
143 | ||
144 | data TagDescription = forall t. TagDescription | |
145 | { tdName :: T.Text | |
146 | , tdArgs :: Args t | |
147 | , tdAction :: t -> (Fragment -> HtmlE) -> HtmlE | |
148 | } | |
149 | ||
150 | simpleTag :: T.Text -> (Markup -> Html) -> TagDescription | |
151 | simpleTag name tag = TagDescription | |
152 | { tdName = name | |
153 | , tdArgs = FragmentArg EndArg | |
154 | , tdAction = \ (fragment :=> ()) f -> | |
155 | fmap (tag . sequence_) (mapM f fragment) | |
156 | } | |
157 | ||
158 | listTag :: T.Text -> (Markup -> Html) -> TagDescription | |
159 | listTag name tag = TagDescription | |
160 | { tdName = name | |
161 | , tdArgs = ListArg | |
162 | , tdAction = \ rs f -> | |
163 | fmap (tag . sequence_) (mapM f (concat rs)) | |
164 | } | |
165 | ||
166 | 122 | |
167 | 123 | -- The built-in set of tags (subject to change) |
168 | 124 | basicTags :: [TagDescription] |
185 | 141 | , listTag "ol" ol |
186 | 142 | , listTag "center" (\ rs -> div ! class_ "center" $ rs) |
187 | 143 | |
188 | , TagDescription "br" EndArg (\ () _ -> return br) | |
189 | , TagDescription "comment" ListArg (\ _ _ -> return "") | |
190 | , TagDescription "link" (TextArg (FragmentArg EndArg)) $ \ (l :=> r :=> ()) f -> | |
191 | let go h = a ! href (toValue l) $ h | |
192 | in fmap (go . sequence_) (mapM f r) | |
193 | , TagDescription "img" (TextArg (TextArg EndArg)) $ \ (l :=> r :=> ()) _ -> | |
194 |
|
|
144 | , TagDescription "br" br | |
145 | , TagDescription "comment" ("" :: Html) | |
146 | , TagDescription "link" (\ (Str l) (H h) -> a ! href (toValue l) $ h) | |
147 | , TagDescription "img" $ \ (Str l) altText -> case altText of | |
148 | Just (Str r) -> img ! src (toValue l) ! alt (toValue r) | |
149 | Nothing -> img ! src (toValue l) | |
195 | 150 | ] |
151 | ||
152 | simpleTag :: T.Text -> (Markup -> Html) -> TagDescription | |
153 | simpleTag name tag = mkTag name (\ (H h) -> tag h) | |
154 | ||
155 | listTag :: T.Text -> (Markup -> Html) -> TagDescription | |
156 | listTag name tag = mkTag name (\ (Hs hs) -> tag hs) | |
196 | 157 | |
197 | 158 | -- render a single paragraph |
198 | 159 | renderPara :: [TagDescription] -> Document -> Either String Html |
199 | 160 | renderPara taglist ds = fmap (p . sequence_) (mapM go ds) |
200 | 161 | where go (TextFrag ts) = Right (toMarkup ts) |
201 | 162 | go (TagFrag (Tag tx rs)) = exec tx rs taglist |
202 | exec name args (TagDescription tag rg func:_) | |
203 | | name == tag = case matchArguments rg args of | |
204 | Nothing -> Left $ unwords [ "Tag" | |
205 | , T.unpack ('\\' `T.cons` name) | |
206 | , "expects argument structure" | |
207 | , T.unpack ('\\' `T.cons` name `T.append` argType rg) | |
208 | ] | |
209 | Just x -> func x go | |
210 | exec name args (_:tags) = exec name args tags | |
211 | exec name args [] = Left $ | |
212 | "Error: no match for tag " ++ T.unpack name ++ "/" ++ show (length args) | |
213 | ||
214 | -- The built-in set of tags (subject to change) | |
215 | basicTags' :: [Tag'] | |
216 | basicTags' = | |
217 | [ simpleTag' "em" em | |
218 | , simpleTag' "strong" strong | |
219 | , simpleTag' "li" li | |
220 | , simpleTag' "h1" h1 | |
221 | , simpleTag' "h2" h2 | |
222 | , simpleTag' "p" (\ rs -> span ! class_ "para" $ rs) | |
223 | , simpleTag' "blockquote" blockquote | |
224 | , simpleTag' "tt" code | |
225 | , simpleTag' "code" (pre . code) | |
226 | , simpleTag' "ttcom" (\ rs -> span ! class_ "comment" $ rs) | |
227 | , simpleTag' "ttkw" (\ rs -> span ! class_ "keyword" $ rs) | |
228 | , simpleTag' "ttcn" (\ rs -> span ! class_ "constr" $ rs) | |
229 | , simpleTag' "ttstr" (\ rs -> span ! class_ "string" $ rs) | |
230 | ||
231 | , listTag' "ul" ul | |
232 | , listTag' "ol" ol | |
233 | , listTag' "center" (\ rs -> div ! class_ "center" $ rs) | |
234 | ||
235 | , Tag' "br" (\ () -> return br) | |
236 | , Tag' "comment" (\ () -> return "") | |
237 | , Tag' "link" (\ (Str l) (H h) () -> | |
238 | let go h = a ! href (toValue l) $ h | |
239 | in return (go h)) | |
240 | , Tag' "img" (\ (Str l) (Str r) () -> | |
241 | return (img ! src (toValue l) ! alt (toValue r))) | |
242 | ] | |
243 | ||
244 | simpleTag' :: T.Text -> (Markup -> Html) -> Tag' | |
245 | simpleTag' name tag = Tag' | |
246 | { tgName = name | |
247 | , tgFunc = \ (H h) () -> | |
248 | return (tag h) | |
249 | } | |
250 | ||
251 | listTag' :: T.Text -> (Markup -> Html) -> Tag' | |
252 | listTag' name tag = Tag' | |
253 | { tgName = name | |
254 | , tgFunc = \ (Hs hs) -> return (tag hs) | |
255 | } | |
256 | ||
257 | -- render a single paragraph | |
258 | renderPara' :: [Tag'] -> Document -> Either String Html | |
259 | renderPara' taglist ds = fmap (p . sequence_) (mapM go ds) | |
260 | where go (TextFrag ts) = Right (toMarkup ts) | |
261 | go (TagFrag (Tag tx rs)) = exec tx rs taglist |