hlint
[diohsc.git] / TextGemini.hs
blob4d5b4131f4d16d26d2d57b754af4d4d3c268bcad
1 -- This file is part of Diohsc
2 -- Copyright (C) 2020 Martin Bays <mbays@sdf.org>
3 --
4 -- This program is free software: you can redistribute it and/or modify
5 -- it under the terms of version 3 of the GNU General Public License as
6 -- published by the Free Software Foundation, or any later version.
7 --
8 -- You should have received a copy of the GNU General Public License
9 -- along with this program. If not, see http://www.gnu.org/licenses/.
11 {-# LANGUAGE CPP #-}
12 {-# LANGUAGE OverloadedStrings #-}
13 {-# LANGUAGE Safe #-}
15 module TextGemini where
17 import Control.Monad.State
18 import Data.Maybe (catMaybes, isJust, mapMaybe)
20 import ANSIColour
22 import qualified Data.Text.Lazy as T
23 import URI
25 data Link = Link { linkUri :: URIRef, linkDescription :: T.Text }
26 deriving (Eq,Ord,Show)
28 data GeminiLine
29 = TextLine T.Text
30 | LinkLine { linkLineIndex :: Int, linkLineLink :: Link }
31 | AltTextLine T.Text
32 | PreformatToggleLine
33 | PreformattedLine T.Text T.Text
34 | HeadingLine Int T.Text
35 | ItemLine T.Text
36 | QuoteLine T.Text
37 | ErrorLine T.Text
38 deriving (Eq,Ord,Show)
40 newtype GeminiDocument = GeminiDocument { geminiDocumentLines :: [GeminiLine] }
41 deriving (Eq,Ord,Show)
43 extractLinks :: GeminiDocument -> [Link]
44 extractLinks (GeminiDocument ls) = mapMaybe linkOfLine ls
45 where
46 linkOfLine (LinkLine _ link) = Just link
47 linkOfLine _ = Nothing
49 data PreOpt
50 = PreOptAlt
51 | PreOptPre
52 | PreOptBoth
53 deriving (Eq,Ord,Show)
55 showPreOpt :: PreOpt -> String
56 showPreOpt PreOptAlt = "alt"
57 showPreOpt PreOptPre = "pre"
58 showPreOpt PreOptBoth = "both"
60 data GemRenderOpts = GemRenderOpts
61 { grOptsAnsi :: Bool
62 , grOptsPre :: PreOpt
63 , grOptsWrapWidth :: Int
64 , grOptsLinkDescFirst :: Bool
65 } deriving (Eq,Ord,Show)
67 printGemDoc :: GemRenderOpts -> (URIRef -> T.Text) -> GeminiDocument -> [T.Text]
68 printGemDoc (GemRenderOpts ansi preOpt width linkDescFirst)
69 showUri (GeminiDocument doc) = concatMap printLine doc
70 where
71 printLine (TextLine line) = wrapWith "" False line
72 printLine (AltTextLine line)
73 | preOpt == PreOptPre || T.null line = []
74 | otherwise = (:[]) $ applyIf ansi withBoldStr "`` " <> line
76 printLine PreformatToggleLine = []
78 printLine (PreformattedLine alt line)
79 | preOpt == PreOptAlt && not (T.null alt) = []
80 | otherwise = (:[]) $ applyIf ansi ((resetCode <>) . withBoldStr) "` " <> line
81 printLine (HeadingLine level line) =
82 wrapWith (T.take (fromIntegral level) (T.repeat '#') <> " ") False
83 . applyIf ansi
84 ( applyIf (level /= 2) withUnderlineStr
85 . applyIf (level < 3) withBoldStr ) $ line
86 printLine (ItemLine line) = wrapWith "* " False line
87 printLine (QuoteLine line) = wrapWith "> " True line
88 printLine (ErrorLine line) = (:[]) $ applyIf ansi (withColourStr Red)
89 "! Formatting error in text/gemini: " <> line
90 printLine (LinkLine n (Link uri desc)) =
91 wrapWith (T.pack $ '[' : show (n+1) ++ if n+1 < 10 then "] " else "] ") False
92 $ (if T.null desc then id
93 else (if linkDescFirst then id else flip) (\a b -> a <> " " <> b)
94 $ applyIf ansi (withColourStr Cyan) desc)
95 (showUri uri)
97 wrapWith :: T.Text -> Bool -> T.Text -> [T.Text]
98 wrapWith pre onAll line =
99 concat . zipWith prependHeader lineHeaders $ wrap (width - n) line
100 where
101 n = visibleLength pre
102 lineHeaders = (pre:) . repeat $
103 if onAll then pre else T.replicate (fromIntegral n) " "
104 splitWordHeader = if n > 0 then "|" <> T.replicate (fromIntegral n - 1) " " else ""
105 prependHeader header (l:ls) = header <> l : ((splitWordHeader <>) <$> ls)
106 prependHeader _ [] = []
108 wrap :: Int -> T.Text -> [[T.Text]]
109 wrap wrapWidth line = wrap' "" 0 $ T.words line
110 where
111 maxWCWidth = 2
112 ww = max maxWCWidth wrapWidth
113 wrap' l n ws | n > ww =
114 chunkVisible l : if null ws then [] else wrap' "" 0 ws
115 where
116 chunkVisible s | T.null s = []
117 chunkVisible s = let (a,b) = splitAtVisible ww s in a : chunkVisible b
118 wrap' l _ [] = [[l]]
119 wrap' l n (w:ws) =
120 let l' = if T.null l then w else l <> " " <> w
121 nw = visibleLength w
122 n' = n + nw + (if T.null l then 0 else 1)
123 in if n' > ww
124 then (if T.null l then id else ([l]:)) $ wrap' w nw ws
125 else wrap' l' n' ws
128 data GeminiParseState = GeminiParseState { numLinks :: Int, preformatted :: Maybe T.Text }
130 initialParseState :: GeminiParseState
131 initialParseState = GeminiParseState 0 Nothing
133 parseGemini :: T.Text -> GeminiDocument
134 parseGemini text = GeminiDocument . catMaybes $ evalState
135 (forM (T.lines text) (parseLine . stripTrailingCR)) initialParseState
136 where
137 stripTrailingCR = T.dropWhileEnd (== '\r')
138 parseLine :: T.Text -> State GeminiParseState (Maybe GeminiLine)
139 parseLine line = do
140 pre <- gets preformatted
141 case T.take 1 line of
142 "`" | T.take 3 line == "```", isJust pre -> do
143 modify $ \s -> s { preformatted = Nothing }
144 return $ case T.strip $ T.drop 3 line of
145 "" -> Nothing
146 _ -> Just . ErrorLine $ "Illegal non-empty text after closing '```'"
147 -- ^The spec says we MUST ignore any text on a "```" line closing a preformatted
148 -- block. This seems like a gaping extensibility hole to me, so I'm interpreting it
149 -- as not disallowing an error message.
150 "`" | T.take 3 line == "```" ->
151 let alt = T.strip $ T.drop 3 line in do
152 modify $ \s -> s { preformatted = Just alt }
153 return . Just . AltTextLine $ alt
154 _ | Just alt <- pre ->
155 return . Just $ PreformattedLine alt line
156 "=" | T.take 2 line == "=>" ->
157 case parseLink . T.dropWhile isGemWhitespace $ T.drop 2 line of
158 Nothing -> return . Just . ErrorLine $ "Unparseable link line: " <> line
159 Just link -> do
160 n <- gets numLinks
161 modify $ \s -> s { numLinks = n + 1 }
162 return . Just $ LinkLine n link
163 "#" | headers <- T.length . T.takeWhile (== '#') $ line,
164 headers > 0 && headers < 4 ->
165 return . Just . HeadingLine (fromIntegral headers) .
166 T.dropWhile isGemWhitespace . T.dropWhile (== '#') $ line
167 "*" | T.take 2 line == "* " ->
168 return . Just . ItemLine $ T.drop 2 line
169 ">" ->
170 return . Just . QuoteLine $ T.drop 1 line
171 _ ->
172 return . Just $ TextLine line
174 parseLink :: T.Text -> Maybe Link
175 parseLink linkInfo =
176 let uriText = T.takeWhile (not . isGemWhitespace) linkInfo
177 desc = T.dropWhile isGemWhitespace . T.dropWhile (not . isGemWhitespace) $ linkInfo
178 in (`Link` desc) <$> parseUriReference (
179 #ifdef IRILinks
180 escapeIRI $
181 #endif
182 T.unpack uriText)
184 isGemWhitespace :: Char -> Bool
185 isGemWhitespace = (`elem` (" \t"::String))