packages feed

slate 0.11.0.0 → 0.12.0.0

raw patch · 9 files changed

+356/−272 lines, 9 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- AnsiStyle: instance GHC.Show.Show AnsiStyle.Style
- AnsiStyle: instance GHC.Show.Show AnsiStyle.Text
- AnsiStyle: toAnsi :: String -> String
- Lib: instance GHC.Classes.Eq Lib.Command
- Lib: instance GHC.Show.Show Lib.Command
- Lib: instance Lib.GetConfig (GHC.Base.Maybe GHC.Base.String)
- Lib: instance Lib.GetConfig GHC.Base.String
+ Ansi: bold :: String
+ Ansi: crossed :: String
+ Ansi: green :: String
+ Ansi: grey :: String
+ Ansi: inverse :: String
+ Ansi: makeBold :: String -> String
+ Ansi: makeCrossed :: String -> String
+ Ansi: makeGreen :: String -> String
+ Ansi: makeGrey :: String -> String
+ Ansi: makeInverse :: String -> String
+ Ansi: makeRed :: String -> String
+ Ansi: makeResetEmphasis :: String -> String
+ Ansi: makeResetUnderline :: String -> String
+ Ansi: makeUnderline :: String -> String
+ Ansi: progress :: Double -> Int -> String
+ Ansi: red :: String
+ Ansi: reset :: String
+ Ansi: resetEmphasis :: String
+ Ansi: resetUnderline :: String
+ Ansi: underline :: String
+ Command: Add :: Slate -> Note -> Command
+ Command: Display :: Slate -> Filter -> Command
+ Command: Doing :: Slate -> (Maybe NoteId) -> Command
+ Command: Done :: Slate -> (Maybe NoteId) -> Command
+ Command: Remove :: Slate -> NoteId -> Command
+ Command: Rename :: Slate -> Slate -> Command
+ Command: Status :: Slate -> Command
+ Command: Sync :: Command
+ Command: Todo :: Slate -> (Maybe NoteId) -> Command
+ Command: Wipe :: Slate -> Filter -> Command
+ Command: data Command
+ Command: instance GHC.Classes.Eq Command.Command
+ Command: instance GHC.Show.Show Command.Command
+ Command: parser :: Parser Command
+ Config: configDirectory :: IO String
+ Config: getConfigValue :: GetConfig a => (String, String) -> IO a
+ Config: getSlatePath :: String -> IO FilePath
+ Config: instance Config.GetConfig (GHC.Base.Maybe GHC.Base.String)
+ Config: instance Config.GetConfig GHC.Base.String
+ Filter: doing :: String -> Bool
+ Filter: done :: String -> Bool
+ Filter: todo :: String -> Bool
+ Style: instance GHC.Show.Show Style.Style
+ Style: instance GHC.Show.Show Style.Text
+ Style: preen :: String -> String

Files

README.md view
@@ -18,7 +18,7 @@  ## Install -You can either download a release from [here](https://github.com/evuez/slate/releases) or install it using [stack](https://docs.haskellstack.org/) (requires stack >= 1.6).+You can install it using [stack](https://docs.haskellstack.org/) (requires stack >= 1.6).  ```shell $ stack install slate
slate.cabal view
@@ -2,10 +2,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: d019482639897778ad0de66e9777ed7e55617c47cb7c5e4fa748a230bc2ad249+-- hash: 1391bb62af66f9a2e775ecb379056cf77456c27fc59b7f928cb629cb9acb7806  name:           slate-version:        0.11.0.0+version:        0.12.0.0 synopsis:       A note taking CLI tool. description:    Please see the README on Github at <https://github.com/evuez/slate#readme> homepage:       https://github.com/evuez/slate#readme@@ -38,8 +38,12 @@     , string-conversions >=0.2     , unordered-containers >=0.2   exposed-modules:-      AnsiStyle+      Ansi+      Command+      Config+      Filter       Lib+      Style   other-modules:       Paths_slate   default-language: Haskell2010
+ src/Ansi.hs view
@@ -0,0 +1,67 @@+module Ansi where++progress :: Double -> Int -> String+progress percent size =+  makeInverse+    ((replicate sizeCompleted ' ') ++ makeGrey (replicate sizeRemaining ' '))+  where+    size' = fromIntegral size :: Double+    sizeCompleted = round $ size' * percent / 100+    sizeRemaining = size - sizeCompleted++reset :: String+reset = "\x1B[0m"++bold :: String+bold = "\x1B[1m"++underline :: String+underline = "\x1B[4m"++inverse :: String+inverse = "\x1B[7m"++crossed :: String+crossed = "\x1B[9m"++resetEmphasis :: String+resetEmphasis = "\x1B[22m"++resetUnderline :: String+resetUnderline = "\x1B[24m"++red :: String+red = "\x1B[31m"++green :: String+green = "\x1B[32m"++grey :: String+grey = "\x1B[2;37m"++makeBold :: String -> String+makeBold s = bold ++ s ++ reset++makeUnderline :: String -> String+makeUnderline s = underline ++ s ++ reset++makeInverse :: String -> String+makeInverse s = inverse ++ s ++ reset++makeCrossed :: String -> String+makeCrossed s = crossed ++ s ++ reset++makeResetEmphasis :: String -> String+makeResetEmphasis s = resetEmphasis ++ s ++ reset++makeResetUnderline :: String -> String+makeResetUnderline s = resetUnderline ++ s ++ reset++makeRed :: String -> String+makeRed s = red ++ s ++ reset++makeGreen :: String -> String+makeGreen s = green ++ s ++ reset++makeGrey :: String -> String+makeGrey s = grey ++ s ++ reset
− src/AnsiStyle.hs
@@ -1,33 +0,0 @@-module AnsiStyle-  ( toAnsi-  ) where--data Style-  = Normal-  | Emphasized-  | Code-  deriving (Show)--data Text = Text-  { style :: Style-  , parsed :: String-  , rest :: String-  } deriving (Show)--getStyle :: Style -> Char -> (Style, String)-getStyle Normal '*' = (Emphasized, "\x1B[1m")-getStyle Normal '_' = (Emphasized, "\x1B[1m")-getStyle Normal '`' = (Code, "\x1B[4m")-getStyle Emphasized '*' = (Normal, "\x1B[21m")-getStyle Emphasized '_' = (Normal, "\x1B[21m")-getStyle Code '`' = (Normal, "\x1B[24m")-getStyle s c = (s, [c])--toAnsi :: String -> String-toAnsi s = parsed $ parseNote $ Text Normal [] s--parseNote :: Text -> Text-parseNote (Text s p (c:t)) = do-  let (s1, c1) = getStyle s c-  parseNote $ Text s1 (p ++ c1) t-parseNote (Text s p ([])) = Text s p []
+ src/Command.hs view
@@ -0,0 +1,113 @@+module Command+  ( parser+  , Command(..)+  ) where++import Data.Semigroup ((<>))+import Options.Applicative++type Slate = String++type Note = String++type NoteId = Int++type Filter = String++data Command+  = Add Slate+        Note+  | Done Slate+         (Maybe NoteId)+  | Todo Slate+         (Maybe NoteId)+  | Doing Slate+          (Maybe NoteId)+  | Remove Slate+           NoteId+  | Display Slate+            Filter+  | Rename Slate+           Slate+  | Wipe Slate+         Filter+  | Status Slate+  | Sync+  deriving (Eq, Show)++name :: Parser String+name =+  option+    str+    (long "name" <> short 'n' <> metavar "SLATE" <> help "Name of the slate." <>+     value "")++add :: Parser Command+add = Add <$> name <*> argument str (metavar "NOTE")++done :: Parser Command+done = Done <$> name <*> optional (argument auto (metavar "NOTE ID"))++todo :: Parser Command+todo = Todo <$> name <*> optional (argument auto (metavar "NOTE ID"))++doing :: Parser Command+doing = Doing <$> name <*> optional (argument auto (metavar "NOTE ID"))++remove :: Parser Command+remove = Remove <$> name <*> argument auto (metavar "NOTE ID")++display :: Parser Command+display =+  Display <$> name <*>+  option+    str+    (long "only" <> short 'o' <> help "Display only done / todo notes." <>+     value "")++rename :: Parser Command+rename =+  Rename <$> argument str (metavar "CURRENT" <> help "Current name.") <*>+  argument str (metavar "NEW" <> help "New name.")++wipe :: Parser Command+wipe =+  Wipe <$> name <*>+  option+    str+    (long "only" <> short 'o' <> help "Wipe only done / todo notes." <> value "")++status :: Parser Command+status = Status <$> name++sync :: Command+sync = Sync++parser :: Parser Command+parser =+  subparser+    (command "add" (info add (progDesc "Add a note.")) <>+     command+       "done"+       (info+          done+          (progDesc+             "Mark a note as done when given a note ID, display done notes otherwise.")) <>+     command+       "todo"+       (info+          todo+          (progDesc+             "Mark a note as todo when given a note ID, display todo notes otherwise.")) <>+     command+       "doing"+       (info+          doing+          (progDesc+             "Toggle highlighting on a note when given a note ID, display notes marked as doing otherwise.")) <>+     command "remove" (info remove (progDesc "Remove a note.")) <>+     command "display" (info display (progDesc "Display a slate.")) <>+     command "rename" (info rename (progDesc "Rename a slate.")) <>+     command "wipe" (info wipe (progDesc "Wipe a slate.")) <>+     command "status" (info status (progDesc "Display the status of a slate.")) <>+     command "sync" (info (pure sync) (progDesc "Sync every slate.")))
+ src/Config.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}++module Config+  ( getConfigValue+  , configDirectory+  , getSlatePath+  ) where++import qualified Data.HashMap.Lazy as M (lookup)+import Data.Maybe (listToMaybe)+import Data.String (fromString)+import Data.String.Conversions (convertString)+import System.Directory (doesFileExist, getCurrentDirectory, getHomeDirectory)+import System.FilePath.Posix (takeBaseName)+import Text.Toml (parseTomlDoc)+import Text.Toml.Types (Node(VString), Node(VTable))++class GetConfig a where+  getConfigValue :: (String, String) -> IO a++instance GetConfig (Maybe String) where+  getConfigValue (s, k) = do+    f <- configFile+    config <- readFile f+    let Right c = parseTomlDoc "" (fromString config)+    return $+      case (M.lookup (fromString s) c) of+        Just (VTable t) ->+          case (M.lookup (fromString k) t) of+            Just (VString v) -> Just (convertString v)+            _ -> Nothing+        _ -> Nothing++instance GetConfig String where+  getConfigValue (s, k) = do+    f <- configFile+    c <- getConfigValue (s, k)+    return $ maybe (error $ "Key `" ++ k ++ "` not found in " ++ f ++ ".") id c++configDirectory :: IO String+configDirectory = do+  home <- getHomeDirectory+  return $ home ++ "/.config/slate/"++configFile :: IO String+configFile = do+  dir <- configDirectory+  return $ dir ++ "config.toml"++slateName :: IO String+slateName = do+  d <- getCurrentDirectory+  let headOrFail =+        \x ->+          maybe+            (error "The .slate file in this directory shouldn't be empty.")+            id+            (listToMaybe x)+  doesFileExist (d ++ "/.slate") >>= \case+    True -> readFile (d ++ "/.slate") >>= (return . headOrFail . lines)+    False -> return $ takeBaseName d++getSlatePath :: String -> IO FilePath+getSlatePath "" = do+  s <- slateName+  dir <- configDirectory+  return $ dir ++ s ++ ".md"+getSlatePath s = do+  dir <- configDirectory+  return $ dir ++ s ++ ".md"
+ src/Filter.hs view
@@ -0,0 +1,18 @@+module Filter+  ( done+  , todo+  , doing+  ) where++done :: String -> Bool+done (' ':'-':' ':'[':'x':']':_) = True+done ('\x1B':'[':'9':'m':_) = True+done _ = False++todo :: String -> Bool+todo n = (not . done) n++doing :: String -> Bool+doing (' ':'-':' ':'[':' ':']':' ':'>':_) = True+doing ('\x1B':'[':'7':'m':_) = True+doing _ = False
src/Lib.hs view
@@ -1,29 +1,16 @@-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE LambdaCase #-}- module Lib   ( initialize   , execute   , parser   ) where -import AnsiStyle (toAnsi)-import qualified Data.HashMap.Lazy as M (lookup)-import Data.Maybe (listToMaybe)-import Data.Semigroup ((<>))-import Data.String (fromString)-import Data.String.Conversions (convertString)-import Options.Applicative-import System.Directory-  ( createDirectoryIfMissing-  , doesFileExist-  , getCurrentDirectory-  , getHomeDirectory-  , removeFile-  , renameFile-  )+import Ansi (makeCrossed, makeGreen, makeInverse, makeRed, progress)+import Command (Command(..), parser)+import Config (configDirectory, getConfigValue, getSlatePath)+import qualified Filter as F (doing, done, todo)+import Style (preen)+import System.Directory (createDirectoryIfMissing, removeFile, renameFile) import System.Exit (ExitCode(ExitFailure, ExitSuccess))-import System.FilePath.Posix (takeBaseName) import System.Process   ( StdStream(NoStream)   , createProcess@@ -33,117 +20,7 @@   , std_out   , waitForProcess   )-import Text.Toml (parseTomlDoc)-import Text.Toml.Types (Node(VString), Node(VTable)) -type Slate = String--type Note = String--type NoteId = Int--type Filter = String--data Command-  = Add Slate-        Note-  | Done Slate-         (Maybe NoteId)-  | Todo Slate-         (Maybe NoteId)-  | Doing Slate-          (Maybe NoteId)-  | Remove Slate-           NoteId-  | Display Slate-            Filter-  | Rename Slate-           Slate-  | Wipe Slate-         Filter-  | Status Slate-  | Sync-  deriving (Eq, Show)---- Parsers-name :: Parser String-name =-  option-    str-    (long "name" <> short 'n' <> metavar "SLATE" <> help "Name of the slate." <>-     value "")--add :: Parser Command-add = Add <$> name <*> argument str (metavar "NOTE")--done :: Parser Command-done = Done <$> name <*> optional (argument auto (metavar "NOTE ID"))--todo :: Parser Command-todo = Todo <$> name <*> optional (argument auto (metavar "NOTE ID"))--doing :: Parser Command-doing = Doing <$> name <*> optional (argument auto (metavar "NOTE ID"))--remove :: Parser Command-remove = Remove <$> name <*> argument auto (metavar "NOTE ID")--display :: Parser Command-display =-  Display <$> name <*>-  option-    str-    (long "only" <> short 'o' <> help "Display only done / todo notes." <>-     value "")--rename :: Parser Command-rename =-  Rename <$> argument str (metavar "CURRENT" <> help "Current name.") <*>-  argument str (metavar "NEW" <> help "New name.")--wipe :: Parser Command-wipe =-  Wipe <$> name <*>-  option-    str-    (long "only" <> short 'o' <> help "Wipe only done / todo notes." <> value "")--status :: Parser Command-status = Status <$> name--sync :: Command-sync = Sync--parser :: Parser Command-parser =-  subparser-    (command "add" (info add (progDesc "Add a note.")) <>-     command-       "done"-       (info-          done-          (progDesc-             "Mark a note as done when given a note ID, display done notes otherwise.")) <>-     command-       "todo"-       (info-          todo-          (progDesc-             "Mark a note as todo when given a note ID, display todo notes otherwise.")) <>-     command-       "doing"-       (info-          doing-          (progDesc-             "Toggle highlighting on a note when given a note ID, display notes marked as doing otherwise.")) <>-     command "remove" (info remove (progDesc "Remove a note.")) <>-     command "display" (info display (progDesc "Display a slate.")) <>-     command "rename" (info rename (progDesc "Rename a slate.")) <>-     command "wipe" (info wipe (progDesc "Wipe a slate.")) <>-     command "status" (info status (progDesc "Display the status of a slate.")) <>-     command "sync" (info (pure sync) (progDesc "Sync every slate.")))---- Commands execute :: Command -> IO () execute (Add s n) =   getSlatePath s >>= (\x -> appendFile x (" - [ ] " ++ n ++ "\n"))@@ -161,106 +38,39 @@ execute (Status s) = getSlatePath s >>= (\x -> displayStatus x) execute (Sync) = syncSlates --- Helpers initialize :: IO ()-initialize = getConfigDirectory >>= (\c -> createDirectoryIfMissing True c)--getSlateName :: IO String-getSlateName = do-  d <- getCurrentDirectory-  let headOrFail =-        \x ->-          maybe-            (error "The .slate file in this directory shouldn't be empty.")-            id-            (listToMaybe x)-  doesFileExist (d ++ "/.slate") >>= \case-    True -> readFile (d ++ "/.slate") >>= (return . headOrFail . lines)-    False -> return $ takeBaseName d--getConfigDirectory :: IO String-getConfigDirectory = do-  home <- getHomeDirectory-  return $ home ++ "/.config/slate/"--getConfigFile :: IO String-getConfigFile = do-  dir <- getConfigDirectory-  return $ dir ++ "config.toml"--getSlatePath :: String -> IO FilePath-getSlatePath "" = do-  s <- getSlateName-  dir <- getConfigDirectory-  return $ dir ++ s ++ ".md"-getSlatePath s = do-  dir <- getConfigDirectory-  return $ dir ++ s ++ ".md"--class GetConfig a where-  getConfigValue :: (String, String) -> IO a--instance GetConfig (Maybe String) where-  getConfigValue (s, k) = do-    f <- getConfigFile-    config <- readFile f-    let Right c = parseTomlDoc "" (fromString config)-    return $-      case (M.lookup (fromString s) c) of-        Just (VTable t) ->-          case (M.lookup (fromString k) t) of-            Just (VString v) -> Just (convertString v)-            _ -> Nothing-        _ -> Nothing--instance GetConfig String where-  getConfigValue (s, k) = do-    f <- getConfigFile-    c <- getConfigValue (s, k)-    return $ maybe (error $ "Key `" ++ k ++ "` not found in " ++ f ++ ".") id c+initialize = configDirectory >>= (\c -> createDirectoryIfMissing True c)  displaySlate :: String -> String -> IO () displaySlate s "" = do   contents <- readFile s-  let notes = zipWith displayNote [0 ..] (lines contents)-  putStr $ unlines notes+  putStr $ unlines $ displayNotes (lines contents) displaySlate s "done" = do   contents <- readFile s-  let notes = zipWith displayNote [0 ..] (lines contents)-  putStr $ unlines $ filter isNoteDone notes+  putStr $ unlines $ filter F.done $ displayNotes (lines contents) displaySlate s "todo" = do   contents <- readFile s-  let notes = zipWith displayNote [0 ..] (lines contents)-  putStr $ unlines $ filter (not . isNoteDone) notes+  putStr $ unlines $ filter F.todo $ displayNotes (lines contents) displaySlate s "doing" = do   contents <- readFile s-  let notes = zipWith displayNote [0 ..] (lines contents)-  putStr $ unlines $ filter isNoteDoing notes+  putStr $ unlines $ filter F.doing $ displayNotes (lines contents) displaySlate _ f = putStrLn $ "\"" ++ f ++ "\" is not a valid filter." -displayNote :: Int -> String -> String-displayNote line (' ':'-':' ':'[':_:']':' ':'>':note) =-  "\x1B[7m" ++ padInt line 2 ++ " -" ++ (toAnsi note) ++ "\x1B[0m"-displayNote line (' ':'-':' ':'[':' ':']':note) =-  padInt line 2 ++ " -" ++ (toAnsi note)-displayNote line (' ':'-':' ':'[':'x':']':note) =-  "\x1B[9m" ++ padInt line 2 ++ " -" ++ (toAnsi note) ++ "\x1B[0m"-displayNote line _ =-  "\x1B[31m" ++-  padInt line 2 ++ " - Parsing error: line is malformed" ++ "\x1B[0m"--isNoteDone :: String -> Bool-isNoteDone (' ':'-':' ':'[':'x':']':_) = True-isNoteDone ('\x1B':'[':'9':'m':_) = True-isNoteDone _ = False+displayNotes :: [String] -> [String]+displayNotes notes = zipWith (displayNote $ length notes) [0 ..] notes -isNoteDoing :: String -> Bool-isNoteDoing (' ':'-':' ':'[':' ':']':' ':'>':_) = True-isNoteDoing ('\x1B':'[':'7':'m':_) = True-isNoteDoing _ = False+displayNote :: Int -> Int -> String -> String+displayNote total line (' ':'-':' ':'[':_:']':' ':'>':note) =+  makeInverse $ alignRight total line ++ " -" ++ preen note+displayNote total line (' ':'-':' ':'[':' ':']':note) =+  alignRight total line ++ " -" ++ preen note+displayNote total line (' ':'-':' ':'[':'x':']':note) =+  makeCrossed $ alignRight total line ++ " -" ++ preen note+displayNote total line _ =+  makeRed $ alignRight total line ++ " - Parsing error: line is malformed" -padInt :: Int -> Int -> String-padInt n s = replicate (s - length (show n)) '0' ++ show n+alignRight :: Int -> Int -> String+alignRight x n = replicate (length (show x) - length (show n)) ' ' ++ show n  markAsDone :: FilePath -> Int -> IO () markAsDone s n = do@@ -325,12 +135,12 @@ wipeSlate s "done" = do   contents <- readFile s   let tmp = s ++ ".tmp"-  writeFile tmp $ unlines $ filter (not . isNoteDone) (lines contents)+  writeFile tmp $ unlines $ filter F.todo (lines contents)   renameFile tmp s wipeSlate s "todo" = do   contents <- readFile s   let tmp = s ++ ".tmp"-  writeFile tmp $ unlines $ filter isNoteDone (lines contents)+  writeFile tmp $ unlines $ filter F.done (lines contents)   renameFile tmp s wipeSlate _ f = putStrLn $ "\"" ++ f ++ "\" is not a valid filter." @@ -338,27 +148,26 @@ displayStatus s = do   ss <- getSyncStatus s   contents <- readFile s-  let d = length $ filter isNoteDone (lines contents)-      t = length $ filter (not . isNoteDone) (lines contents)-      dd = fromIntegral d :: Double-      dt = fromIntegral t :: Double-      pd = round $ 28 * dd / (dt + dd)-      pt = round $ 28 * dt / (dt + dd)-      p = round $ dd / (dd + dt) * 100 :: Integer-  putStrLn $-    (show d) ++-    " done, " ++-    (show t) ++-    " todo (" ++-    (show p) ++-    "% done).\n" ++ (replicate pd '▮') ++ (replicate pt '▯') ++ "\n" ++ ss+  let done = fromIntegral $ length $ filter F.done (lines contents) :: Double+      todo = fromIntegral $ length $ filter F.todo (lines contents) :: Double+      percent = done / (done + todo) * 100+      stats =+        mconcat+          [ show (round done :: Integer)+          , " done, "+          , show (round todo :: Integer)+          , " todo ("+          , show (round percent :: Integer)+          , "% done)."+          ]+  putStrLn $ mconcat [stats, "\n", progress percent (length stats), "\n", ss]  getSyncStatus :: FilePath -> IO String getSyncStatus s = do   v <- getConfigValue ("callbacks", "status")   case v of     (Just c) -> do-      d <- getConfigDirectory+      d <- configDirectory       (_, _, _, h) <-         createProcess           (shell c)@@ -366,14 +175,14 @@       e <- waitForProcess h       return $         case e of-          ExitSuccess -> "\x1B[32mSynced ☺️\x1B[0m"-          (ExitFailure _) -> "\x1B[31mOut of sync 😕\x1B[0m"+          ExitSuccess -> makeGreen "Synced ☺️"+          (ExitFailure _) -> makeRed "Out of sync 😕"     Nothing -> return ""  syncSlates :: IO () syncSlates = do   c <- getConfigValue ("callbacks", "sync")-  d <- getConfigDirectory+  d <- configDirectory   (_, _, _, h) <- createProcess (shell c) {cwd = Just d}   _ <- waitForProcess h-  putStrLn "\x1B[32mDone syncing ☺️\x1B[0m"+  putStrLn $ makeGreen "Done syncing ☺️"
+ src/Style.hs view
@@ -0,0 +1,35 @@+module Style+  ( preen+  ) where++import Ansi (bold, resetEmphasis, resetUnderline, underline)++data Style+  = Normal+  | Emphasized Char+  | Code+  deriving (Show)++data Text = Text+  { style :: Style+  , parsed :: String+  , rest :: String+  } deriving (Show)++getStyle :: Style -> Char -> (Style, String)+getStyle Normal '*' = (Emphasized '*', bold)+getStyle Normal '_' = (Emphasized '_', bold)+getStyle Normal '`' = (Code, underline)+getStyle (Emphasized '*') '*' = (Normal, resetEmphasis)+getStyle (Emphasized '_') '_' = (Normal, resetEmphasis)+getStyle Code '`' = (Normal, resetUnderline)+getStyle s c = (s, [c])++preen :: String -> String+preen s = parsed $ parseNote $ Text Normal [] s++parseNote :: Text -> Text+parseNote (Text s p (c:t)) = do+  let (s1, c1) = getStyle s c+  parseNote $ Text s1 (p ++ c1) t+parseNote (Text s p ([])) = Text s p []