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/.
11 {-# LANGUAGE OverloadedStrings #-}
14 module TextGemini
where
16 import Control
.Monad
.State
17 import Data
.Maybe (catMaybes, mapMaybe)
21 import qualified Data
.Text
.Lazy
as T
24 data Link
= Link
{ linkUri
:: URIRef
, linkDescription
:: T
.Text
}
25 deriving (Eq
,Ord
,Show)
29 | LinkLine
{ linkLineIndex
:: Int, linkLineLink
:: Link
}
32 | PreformattedLine T
.Text
33 | HeadingLine
Int T
.Text
37 deriving (Eq
,Ord
,Show)
39 newtype GeminiDocument
= GeminiDocument
{ geminiDocumentLines
:: [GeminiLine
] }
40 deriving (Eq
,Ord
,Show)
42 extractLinks
:: GeminiDocument
-> [Link
]
43 extractLinks
(GeminiDocument ls
) = mapMaybe linkOfLine ls
45 linkOfLine
(LinkLine _ link
) = Just link
46 linkOfLine _
= Nothing
52 deriving (Eq
,Ord
,Show)
54 showPreOpt
:: PreOpt
-> String
55 showPreOpt PreOptAlt
= "alt"
56 showPreOpt PreOptPre
= "pre"
57 showPreOpt PreOptBoth
= "both"
59 data GemRenderOpts
= GemRenderOpts
62 , grOptsWrapWidth
:: Int
63 } deriving (Eq
,Ord
,Show)
65 printGemDoc
:: GemRenderOpts
-> (URIRef
-> T
.Text
) -> GeminiDocument
-> [T
.Text
]
66 printGemDoc
(GemRenderOpts ansi preOpt width
)
67 showUri
(GeminiDocument ls
) = concatMap printLine ls
69 printLine
(TextLine line
) = wrap width line
70 printLine
(LinkLine n link
) = (:[]) $
71 printGemLinkLine ansi showUri
(n
+1) link
72 printLine
(AltTextLine line
)
73 | preOpt
== PreOptPre
= []
74 | preOpt
== PreOptBoth
&& T
.null line
= []
75 |
otherwise = (:[]) $ applyIf ansi withBoldStr
"`` " <> line
77 -- |the spec says preformat toggle lines should not be rendered, but it's the most convenient
78 -- non-disruptive way to unambiguously indicate which lines are preformatted.
79 printLine PreformatToggleLine
= []
81 printLine
(PreformattedLine line
)
82 | preOpt
== PreOptAlt
= []
83 |
otherwise = (:[]) $ applyIf ansi
((resetCode
<>) . withBoldStr
) "` " <> line
84 printLine
(HeadingLine level line
) = (:[]) $ if ansi
85 then applyIf
(level
/= 2) withUnderlineStr
.
86 applyIf
(level
< 3) withBoldStr
$ line
87 else T
.take (fromIntegral level
) (T
.repeat '#') <> " " <> line
88 printLine
(ItemLine line
) = wrapWith
"* " " " line
89 printLine
(QuoteLine line
) = wrapWith
"> " "> " line
90 printLine
(ErrorLine line
) = (:[]) $ applyIf ansi
(withColourStr Red
)
91 "! Formatting error in text/gemini: " <> line
93 wrapWith first subsequent line
=
94 zipWith (<>) lineHeaders
$ wrap
(width
- fromIntegral (T
.length first
)) line
95 where lineHeaders
= map (applyIf ansi withBoldStr
) $ first
: repeat subsequent
97 wrap
:: Int -> T
.Text
-> [T
.Text
]
98 wrap wrapWidth line
= wrap
' "" 0 $ T
.words line
102 let l
' = if T
.null l
then w
else l
<> " " <> w
103 nw
= fromIntegral . T
.length $ stripANSI w
104 n
' = n
+ nw
+ (if T
.null l
then 0 else 1)
106 then l
: wrap
' w nw ws
109 printGemLinkLine
:: Bool -> (URIRef
-> T
.Text
) -> Int -> Link
-> T
.Text
110 printGemLinkLine ansi showUri n
(Link uri desc
) =
111 applyIf ansi withBoldStr
(T
.pack
$ '[' : show n
++ "]")
114 <> (if T
.null desc
then "" else
115 applyIf ansi
(withColourStr Cyan
) $ " " <> desc
)
117 data GeminiParseState
= GeminiParseState
{ numLinks
:: Int, preformatted
:: Bool }
119 initialParseState
:: GeminiParseState
120 initialParseState
= GeminiParseState
0 False
122 parseGemini
:: T
.Text
-> GeminiDocument
123 parseGemini text
= GeminiDocument
. catMaybes $ evalState
124 (forM
(T
.lines text
) (parseLine
. stripTrailingCR
)) initialParseState
126 stripTrailingCR
= T
.dropWhileEnd
(== '\r')
127 parseLine
:: T
.Text
-> State GeminiParseState
(Maybe GeminiLine
)
129 pre
<- gets preformatted
130 if T
.take 3 line
== "```" then do
131 modify
$ \s
-> s
{ preformatted
= not pre
}
132 return $ if pre
then case T
.strip
$ T
.drop 3 line
of
134 _
-> Just
. ErrorLine
$ "Illegal non-empty text after closing '```'"
135 -- ^The spec says we MUST ignore any text on a "```" line closing a preformatted
136 -- block. This seems like a gaping extensibility hole to me, so I'm interpreting it
137 -- as not disallowing an error message.
138 else Just
. AltTextLine
. T
.strip
$ T
.drop 3 line
139 else if pre
then return . Just
$ PreformattedLine line
140 else if T
.take 2 line
== "=>" then
141 case parseLink
. T
.dropWhile isGemWhitespace
$ T
.drop 2 line
of
142 Nothing
-> return . Just
. ErrorLine
$ "Unparseable link line: " <> line
145 modify
$ \s
-> s
{ numLinks
= n
+ 1 }
146 return . Just
$ LinkLine n link
147 else let headers
= T
.length . T
.takeWhile (== '#') $ line
148 in if headers
> 0 && headers
< 4
149 then return . Just
. HeadingLine
(fromIntegral headers
) .
150 T
.dropWhile isGemWhitespace
. T
.dropWhile (== '#') $ line
151 else if T
.take 2 line
== "* "
152 then return . Just
. ItemLine
$ T
.drop 2 line
153 else if T
.take 1 line
== ">"
154 then return . Just
. QuoteLine
$ T
.drop 1 line
155 else return . Just
$ TextLine line
157 parseLink
:: T
.Text
-> Maybe Link
159 let uriText
= T
.takeWhile (not . isGemWhitespace
) linkInfo
160 desc
= T
.dropWhile isGemWhitespace
. T
.dropWhile (not . isGemWhitespace
) $ linkInfo
161 in (`Link` desc
) <$> parseUriReference
(T
.unpack uriText
)
163 isGemWhitespace
:: Char -> Bool
164 isGemWhitespace
= (`
elem`
(" \t"::String))