{-# LANGUAGE OverloadedStrings #-}
module Text.Gemini
(
GemDocument
, GemItem (..)
, decode
, decodeLine
, encode
, encodeItem
, documentTitle
) where
import Data.Bool (bool)
import Data.Char (isSpace)
import Data.Maybe (listToMaybe)
import Data.Text (Text)
import qualified Data.Text as T
type GemDocument = [GemItem]
data GemItem
=
GemText !Text
|
GemLink !Text !(Maybe Text)
|
GemHeading !Int !Text
|
GemList ![Text]
|
GemQuote !Text
|
GemPre ![Text] !(Maybe Text)
deriving (Int -> GemItem -> ShowS
[GemItem] -> ShowS
GemItem -> String
(Int -> GemItem -> ShowS)
-> (GemItem -> String) -> ([GemItem] -> ShowS) -> Show GemItem
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> GemItem -> ShowS
showsPrec :: Int -> GemItem -> ShowS
$cshow :: GemItem -> String
show :: GemItem -> String
$cshowList :: [GemItem] -> ShowS
showList :: [GemItem] -> ShowS
Show, GemItem -> GemItem -> Bool
(GemItem -> GemItem -> Bool)
-> (GemItem -> GemItem -> Bool) -> Eq GemItem
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: GemItem -> GemItem -> Bool
== :: GemItem -> GemItem -> Bool
$c/= :: GemItem -> GemItem -> Bool
/= :: GemItem -> GemItem -> Bool
Eq)
decode :: Text -> GemDocument
decode :: Text -> [GemItem]
decode = [Text] -> [GemItem]
parse ([Text] -> [GemItem]) -> (Text -> [Text]) -> Text -> [GemItem]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> [Text]
T.lines (Text -> [Text]) -> (Text -> Text) -> Text -> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. HasCallStack => Text -> Text -> Text -> Text
Text -> Text -> Text -> Text
T.replace Text
"\CR" Text
""
where
parse :: [Text] -> GemDocument
parse :: [Text] -> [GemItem]
parse (Text
l : [Text]
ls)
| Text
"* " Text -> Text -> Bool
`T.isPrefixOf` Text
l =
let ([Text]
items, [Text]
rest) = (Text -> Bool) -> [Text] -> ([Text], [Text])
forall a. (a -> Bool) -> [a] -> ([a], [a])
span (Text
"* " Text -> Text -> Bool
`T.isPrefixOf`) [Text]
ls
in ([Text] -> GemItem
GemList ([Text] -> GemItem) -> ([Text] -> [Text]) -> [Text] -> GemItem
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Text -> Text) -> [Text] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map (Int -> Text -> Text
value Int
2) ([Text] -> GemItem) -> [Text] -> GemItem
forall a b. (a -> b) -> a -> b
$ [Text
l] [Text] -> [Text] -> [Text]
forall a. Semigroup a => a -> a -> a
<> [Text]
items) GemItem -> [GemItem] -> [GemItem]
forall a. a -> [a] -> [a]
: [Text] -> [GemItem]
parse [Text]
rest
| Text
"```" Text -> Text -> Bool
`T.isPrefixOf` Text
l =
let ([Text]
pre, [Text]
rest) = (Text -> Bool) -> [Text] -> ([Text], [Text])
forall a. (a -> Bool) -> [a] -> ([a], [a])
break (Text
"```" Text -> Text -> Bool
`T.isPrefixOf`) [Text]
ls
in case [Text]
rest of
[] -> Text -> GemItem
GemText Text
l GemItem -> [GemItem] -> [GemItem]
forall a. a -> [a] -> [a]
: [Text] -> [GemItem]
parse [Text]
ls
(Text
_ : [Text]
after) -> [Text] -> Maybe Text -> GemItem
GemPre [Text]
pre (Text -> Maybe Text
optional (Text -> Maybe Text) -> (Text -> Text) -> Text -> Maybe Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Text -> Text
value Int
3 (Text -> Maybe Text) -> Text -> Maybe Text
forall a b. (a -> b) -> a -> b
$ Text
l) GemItem -> [GemItem] -> [GemItem]
forall a. a -> [a] -> [a]
: [Text] -> [GemItem]
parse [Text]
after
| Bool
otherwise = Text -> GemItem
decodeLine Text
l GemItem -> [GemItem] -> [GemItem]
forall a. a -> [a] -> [a]
: [Text] -> [GemItem]
parse [Text]
ls
parse [] = []
decodeLine :: Text -> GemItem
decodeLine :: Text -> GemItem
decodeLine Text
line
| Text
"=>" Text -> Text -> Bool
`T.isPrefixOf` Text
sane =
let (Text
link, Text
desc) = (Char -> Bool) -> Text -> (Text, Text)
T.break Char -> Bool
isSpace (Text -> (Text, Text)) -> Text -> (Text, Text)
forall a b. (a -> b) -> a -> b
$ Int -> Text -> Text
value Int
2 Text
sane
in if Text -> Bool
T.null Text
link
then Text -> GemItem
GemText (Text -> GemItem) -> (Text -> Text) -> Text -> GemItem
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
T.stripEnd (Text -> GemItem) -> Text -> GemItem
forall a b. (a -> b) -> a -> b
$ Text
sane
else Text -> Maybe Text -> GemItem
GemLink Text
link (Maybe Text -> GemItem) -> (Text -> Maybe Text) -> Text -> GemItem
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Maybe Text
optional (Text -> Maybe Text) -> (Text -> Text) -> Text -> Maybe Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
T.strip (Text -> GemItem) -> Text -> GemItem
forall a b. (a -> b) -> a -> b
$ Text
desc
| Text
"#" Text -> Text -> Bool
`T.isPrefixOf` Text
sane =
let level :: Int
level = Int -> Int -> Int
forall a. Ord a => a -> a -> a
min (Text -> Int
T.length (Text -> Int) -> Text -> Int
forall a b. (a -> b) -> a -> b
$ (Text, Text) -> Text
forall a b. (a, b) -> a
fst ((Text, Text) -> Text) -> (Text -> (Text, Text)) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char -> Bool) -> Text -> (Text, Text)
T.span (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'#') (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$ Text
sane) Int
3
in Int -> Text -> GemItem
GemHeading Int
level (Text -> GemItem) -> (Text -> Text) -> Text -> GemItem
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Text -> Text
value Int
level (Text -> GemItem) -> Text -> GemItem
forall a b. (a -> b) -> a -> b
$ Text
sane
| Text
"* " Text -> Text -> Bool
`T.isPrefixOf` Text
sane = [Text] -> GemItem
GemList [Int -> Text -> Text
value Int
2 Text
sane]
| Text
">" Text -> Text -> Bool
`T.isPrefixOf` Text
sane = Text -> GemItem
GemQuote (Text -> GemItem) -> (Text -> Text) -> Text -> GemItem
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Text -> Text
value Int
1 (Text -> GemItem) -> Text -> GemItem
forall a b. (a -> b) -> a -> b
$ Text
sane
| Bool
otherwise = Text -> GemItem
GemText (Text -> GemItem) -> (Text -> Text) -> Text -> GemItem
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
T.stripEnd (Text -> GemItem) -> Text -> GemItem
forall a b. (a -> b) -> a -> b
$ Text
sane
where
sane :: Text
sane = Text -> Text
escapeLineBreaks Text
line
encode :: GemDocument -> Text
encode :: [GemItem] -> Text
encode = [Text] -> Text
T.unlines ([Text] -> Text) -> ([GemItem] -> [Text]) -> [GemItem] -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (GemItem -> Text) -> [GemItem] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map ((Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"\CR") (Text -> Text) -> (GemItem -> Text) -> GemItem -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. GemItem -> Text
encodeItem) ([GemItem] -> [Text])
-> ([GemItem] -> [GemItem]) -> [GemItem] -> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (GemItem -> Bool) -> [GemItem] -> [GemItem]
forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
not (Bool -> Bool) -> (GemItem -> Bool) -> GemItem -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. GemItem -> Bool
empty)
where
empty :: GemItem -> Bool
empty (GemList [Text]
list) = [Text] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Text]
list
empty GemItem
_ = Bool
False
encodeItem :: GemItem -> Text
encodeItem :: GemItem -> Text
encodeItem GemItem
item = case GemItem
item of
GemText Text
text ->
let shouldEscape :: Bool
shouldEscape =
(Text -> Bool) -> [Text] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (Text -> Text -> Bool
`T.isPrefixOf` Text
text) [Text
"#", Text
"* ", Text
">"]
Bool -> Bool -> Bool
|| Text
"```" Text -> Text -> Bool
`T.isPrefixOf` Text
text
Bool -> Bool -> Bool
|| (Text
"=>" Text -> Text -> Bool
`T.isPrefixOf` Text
text Bool -> Bool -> Bool
&& Bool -> Bool
not (Text -> Bool
T.null (Text -> Bool) -> Text -> Bool
forall a b. (a -> b) -> a -> b
$ Int -> Text -> Text
value Int
2 Text
text))
in Text -> Text
T.stripEnd (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
escapeLineBreaks (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$ Text -> Text -> Bool -> Text
forall a. a -> a -> Bool -> a
bool Text
text (Char -> Text -> Text
T.cons Char
' ' Text
text) Bool
shouldEscape
GemLink Text
link Maybe Text
desc -> Text
"=> " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> (HasCallStack => Text -> Text -> Text -> Text
Text -> Text -> Text -> Text
T.replace Text
" " Text
"%20" (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
sanitize (Text -> Text) -> Text -> Text
forall a b. (a -> b) -> a -> b
$ Text
link) Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Maybe Text -> Text
append Maybe Text
desc
GemHeading Int
level Text
text -> Int -> Text -> Text
T.replicate (Int -> Int -> Int
forall a. Ord a => a -> a -> a
min Int
level Int
3) Text
"#" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Maybe Text -> Text
append (Text -> Maybe Text
optional Text
text)
GemList [Text]
list -> Text -> [Text] -> Text
T.intercalate Text
"\CR\LF" ([Text] -> Text) -> [Text] -> Text
forall a b. (a -> b) -> a -> b
$ (Text -> Text) -> [Text] -> [Text]
forall a b. (a -> b) -> [a] -> [b]
map ((Text
"* " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<>) (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
sanitize) [Text]
list
GemQuote Text
text -> Text
">" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Maybe Text -> Text
append (Text -> Maybe Text
optional Text
text)
GemPre [Text]
text Maybe Text
alt -> Text -> [Text] -> Text
T.intercalate Text
"\CR\LF" ([Text] -> Text) -> [Text] -> Text
forall a b. (a -> b) -> a -> b
$ [Text
"```" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Maybe Text -> Text
append Maybe Text
alt] [Text] -> [Text] -> [Text]
forall a. Semigroup a => a -> a -> a
<> [Text]
text [Text] -> [Text] -> [Text]
forall a. Semigroup a => a -> a -> a
<> [Text
"```"]
documentTitle :: GemDocument -> Maybe Text
documentTitle :: [GemItem] -> Maybe Text
documentTitle [GemItem]
doc = [Text] -> Maybe Text
forall a. [a] -> Maybe a
listToMaybe [Text
h | GemHeading Int
_ Text
h <- [GemItem]
doc]
optional :: Text -> Maybe Text
optional :: Text -> Maybe Text
optional Text
text = Maybe Text -> Maybe Text -> Bool -> Maybe Text
forall a. a -> a -> Bool -> a
bool (Text -> Maybe Text
forall a. a -> Maybe a
Just Text
text) Maybe Text
forall a. Maybe a
Nothing (Bool -> Maybe Text) -> Bool -> Maybe Text
forall a b. (a -> b) -> a -> b
$ Text -> Bool
T.null Text
text
append :: Maybe Text -> Text
append :: Maybe Text -> Text
append = Text -> (Text -> Text) -> Maybe Text -> Text
forall b a. b -> (a -> b) -> Maybe a -> b
maybe Text
"" ((Text
" " Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<>) (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
sanitize)
value :: Int -> Text -> Text
value :: Int -> Text -> Text
value Int
prefix = Text -> Text
sanitize (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Text -> Text
T.drop Int
prefix
sanitize :: Text -> Text
sanitize :: Text -> Text
sanitize = Text -> Text
T.strip (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Text
escapeLineBreaks
escapeLineBreaks :: Text -> Text
escapeLineBreaks :: Text -> Text
escapeLineBreaks = HasCallStack => Text -> Text -> Text -> Text
Text -> Text -> Text -> Text
T.replace Text
"\LF" Text
" " (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. HasCallStack => Text -> Text -> Text -> Text
Text -> Text -> Text -> Text
T.replace Text
"\CR" Text
""