1 -- This file is part of Diohsc
2 -- Copyright (C) 2020 Martin Bays <mbays@sdf.org>
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.
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/.
12 {-# LANGUAGE OverloadedStrings #-}
15 module TextGemini
where
17 import Control
.Monad
.State
18 import Data
.Maybe (catMaybes, isJust, mapMaybe)
22 import qualified Data
.Text
.Lazy
as T
25 data Link
= Link
{ linkUri
:: URIRef
, linkDescription
:: T
.Text
}
26 deriving (Eq
,Ord
,Show)
30 | LinkLine
{ linkLineIndex
:: Int, linkLineLink
:: Link
}
33 | PreformattedLine T
.Text T
.Text
34 | HeadingLine
Int 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
46 linkOfLine
(LinkLine _ link
) = Just link
47 linkOfLine _
= Nothing
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
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
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
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
)
97 wrapWith
:: T
.Text
-> Bool -> T
.Text
-> [T
.Text
]
98 wrapWith pre onAll line
=
99 concat . zipWith prependHeader lineHeaders
$ wrap
(width
- n
) line
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
112 ww
= max maxWCWidth wrapWidth
113 wrap
' l n ws | n
> ww
=
114 chunkVisible l
: if null ws
then [] else wrap
' "" 0 ws
116 chunkVisible s | T
.null s
= []
117 chunkVisible s
= let (a
,b
) = splitAtVisible ww s
in a
: chunkVisible b
120 let l
' = if T
.null l
then w
else l
<> " " <> w
122 n
' = n
+ nw
+ (if T
.null l
then 0 else 1)
124 then (if T
.null l
then id else ([l
]:)) $ wrap
' w nw 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
137 stripTrailingCR
= T
.dropWhileEnd
(== '\r')
138 parseLine
:: T
.Text
-> State GeminiParseState
(Maybe GeminiLine
)
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
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
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
170 return . Just
. QuoteLine
$ T
.drop 1 line
172 return . Just
$ TextLine line
174 parseLink
:: T
.Text
-> Maybe Link
176 let uriText
= T
.takeWhile (not . isGemWhitespace
) linkInfo
177 desc
= T
.dropWhile isGemWhitespace
. T
.dropWhile (not . isGemWhitespace
) $ linkInfo
178 in (`Link` desc
) <$> parseUriReference
(
184 isGemWhitespace
:: Char -> Bool
185 isGemWhitespace
= (`
elem`
(" \t"::String))