packages feed

org-mode 1.1.1 → 2.0.0

raw patch · 8 files changed

+644/−69 lines, 8 filesdep +timePVP ok

version bump matches the API change (PVP)

Dependencies added: time

API changes (from Hackage documentation)

- Data.Org: Tags :: NonEmpty Text -> Words
- Data.Org: instance Data.Hashable.Class.Hashable Data.Org.Block
- Data.Org: instance Data.Hashable.Class.Hashable Data.Org.Column
- Data.Org: instance Data.Hashable.Class.Hashable Data.Org.Item
- Data.Org: instance Data.Hashable.Class.Hashable Data.Org.Language
- Data.Org: instance Data.Hashable.Class.Hashable Data.Org.ListItems
- Data.Org: instance Data.Hashable.Class.Hashable Data.Org.OrgDoc
- Data.Org: instance Data.Hashable.Class.Hashable Data.Org.Row
- Data.Org: instance Data.Hashable.Class.Hashable Data.Org.Section
+ Data.Org: DONE :: Todo
+ Data.Org: Day :: Interval
+ Data.Org: Delay :: DelayMode -> Word -> Interval -> Delay
+ Data.Org: DelayAll :: DelayMode
+ Data.Org: DelayOne :: DelayMode
+ Data.Org: FromToday :: RepeatMode
+ Data.Org: Hour :: Interval
+ Data.Org: Jump :: RepeatMode
+ Data.Org: Month :: Interval
+ Data.Org: OrgDateTime :: Day -> DayOfWeek -> Maybe OrgTime -> Maybe Repeater -> Maybe Delay -> OrgDateTime
+ Data.Org: OrgTime :: TimeOfDay -> Maybe TimeOfDay -> OrgTime
+ Data.Org: Priority :: Text -> Priority
+ Data.Org: Repeater :: RepeatMode -> Word -> Interval -> Repeater
+ Data.Org: Single :: RepeatMode
+ Data.Org: TODO :: Todo
+ Data.Org: Week :: Interval
+ Data.Org: Year :: Interval
+ Data.Org: [dateDayOfWeek] :: OrgDateTime -> DayOfWeek
+ Data.Org: [dateDay] :: OrgDateTime -> Day
+ Data.Org: [dateDelay] :: OrgDateTime -> Maybe Delay
+ Data.Org: [dateRepeat] :: OrgDateTime -> Maybe Repeater
+ Data.Org: [dateTime] :: OrgDateTime -> Maybe OrgTime
+ Data.Org: [delayInterval] :: Delay -> Interval
+ Data.Org: [delayMode] :: Delay -> DelayMode
+ Data.Org: [delayValue] :: Delay -> Word
+ Data.Org: [priority] :: Priority -> Text
+ Data.Org: [repInterval] :: Repeater -> Interval
+ Data.Org: [repMode] :: Repeater -> RepeatMode
+ Data.Org: [repValue] :: Repeater -> Word
+ Data.Org: [sectionClosed] :: Section -> Maybe OrgDateTime
+ Data.Org: [sectionDeadline] :: Section -> Maybe OrgDateTime
+ Data.Org: [sectionPriority] :: Section -> Maybe Priority
+ Data.Org: [sectionProps] :: Section -> Map Text Text
+ Data.Org: [sectionScheduled] :: Section -> Maybe OrgDateTime
+ Data.Org: [sectionTimestamp] :: Section -> Maybe OrgDateTime
+ Data.Org: [sectionTodo] :: Section -> Maybe Todo
+ Data.Org: [timeEnd] :: OrgTime -> Maybe TimeOfDay
+ Data.Org: [timeStart] :: OrgTime -> TimeOfDay
+ Data.Org: allDocTags :: OrgDoc -> Set Text
+ Data.Org: allSectionTags :: Section -> Set Text
+ Data.Org: data Delay
+ Data.Org: data DelayMode
+ Data.Org: data Interval
+ Data.Org: data OrgDateTime
+ Data.Org: data OrgTime
+ Data.Org: data RepeatMode
+ Data.Org: data Repeater
+ Data.Org: data Todo
+ Data.Org: date :: Parser Day
+ Data.Org: instance GHC.Classes.Eq Data.Org.Delay
+ Data.Org: instance GHC.Classes.Eq Data.Org.DelayMode
+ Data.Org: instance GHC.Classes.Eq Data.Org.Interval
+ Data.Org: instance GHC.Classes.Eq Data.Org.OrgDateTime
+ Data.Org: instance GHC.Classes.Eq Data.Org.OrgTime
+ Data.Org: instance GHC.Classes.Eq Data.Org.Priority
+ Data.Org: instance GHC.Classes.Eq Data.Org.RepeatMode
+ Data.Org: instance GHC.Classes.Eq Data.Org.Repeater
+ Data.Org: instance GHC.Classes.Eq Data.Org.Todo
+ Data.Org: instance GHC.Classes.Ord Data.Org.Block
+ Data.Org: instance GHC.Classes.Ord Data.Org.Column
+ Data.Org: instance GHC.Classes.Ord Data.Org.Delay
+ Data.Org: instance GHC.Classes.Ord Data.Org.DelayMode
+ Data.Org: instance GHC.Classes.Ord Data.Org.Interval
+ Data.Org: instance GHC.Classes.Ord Data.Org.Item
+ Data.Org: instance GHC.Classes.Ord Data.Org.Language
+ Data.Org: instance GHC.Classes.Ord Data.Org.ListItems
+ Data.Org: instance GHC.Classes.Ord Data.Org.OrgDateTime
+ Data.Org: instance GHC.Classes.Ord Data.Org.OrgDoc
+ Data.Org: instance GHC.Classes.Ord Data.Org.OrgFile
+ Data.Org: instance GHC.Classes.Ord Data.Org.OrgTime
+ Data.Org: instance GHC.Classes.Ord Data.Org.Priority
+ Data.Org: instance GHC.Classes.Ord Data.Org.RepeatMode
+ Data.Org: instance GHC.Classes.Ord Data.Org.Repeater
+ Data.Org: instance GHC.Classes.Ord Data.Org.Row
+ Data.Org: instance GHC.Classes.Ord Data.Org.Section
+ Data.Org: instance GHC.Classes.Ord Data.Org.Todo
+ Data.Org: instance GHC.Classes.Ord Data.Org.URL
+ Data.Org: instance GHC.Classes.Ord Data.Org.Words
+ Data.Org: instance GHC.Generics.Generic Data.Org.Priority
+ Data.Org: instance GHC.Generics.Generic Data.Org.Todo
+ Data.Org: instance GHC.Show.Show Data.Org.Delay
+ Data.Org: instance GHC.Show.Show Data.Org.DelayMode
+ Data.Org: instance GHC.Show.Show Data.Org.Interval
+ Data.Org: instance GHC.Show.Show Data.Org.OrgDateTime
+ Data.Org: instance GHC.Show.Show Data.Org.OrgTime
+ Data.Org: instance GHC.Show.Show Data.Org.Priority
+ Data.Org: instance GHC.Show.Show Data.Org.RepeatMode
+ Data.Org: instance GHC.Show.Show Data.Org.Repeater
+ Data.Org: instance GHC.Show.Show Data.Org.Todo
+ Data.Org: newtype Priority
+ Data.Org: properties :: Parser (Map Text Text)
+ Data.Org: property :: Parser (Text, Text)
+ Data.Org: repeater :: Parser Repeater
+ Data.Org: timeRange :: Parser OrgTime
+ Data.Org: timestamp :: Parser OrgDateTime
+ Data.Org: titled :: Words -> Section
- Data.Org: Section :: NonEmpty Words -> [Text] -> OrgDoc -> Section
+ Data.Org: Section :: Maybe Todo -> Maybe Priority -> NonEmpty Words -> [Text] -> Maybe OrgDateTime -> Maybe OrgDateTime -> Maybe OrgDateTime -> Maybe OrgDateTime -> Map Text Text -> OrgDoc -> Section

Files

CHANGELOG.md view
@@ -1,5 +1,26 @@ # Changelog +## Unreleased++This release sees the extension of this library to understand heading+timestamps, TODOs, priorities, and `PROPERTIES` drawers.++#### Added++- A number of new timestamp types.+- `Ord` instances for every type.+- Functions `allDocTags` and `allSectionTags` for extracting unique `Set`s of+  recursive heading tags.++#### Changed++- **Breaking:** `Section` has several new fields involving timestamps and properties.+- **Breaking:** The `Tags` variant has been removed from the `Words` type.++#### Fixed++- Heading tags are properly parsed.+ ## 1.1.1 (2021-04-04)  #### Fixed
LICENSE view
@@ -1,4 +1,4 @@-Copyright Colin Woodbury (c) 2020+Copyright Colin Woodbury (c) 2020 - 2021  All rights reserved. 
README.md view
@@ -8,11 +8,49 @@ - File metadata like [In-buffer   Settings](https://orgmode.org/manual/In_002dbuffer-Settings.html#In_002dbuffer-Settings)   appears at the top of the file.+- Special timestamps like `DEADLINE` always appear in the order `CLOSED`,+  `DEADLINE`, then `SCHEDULED` if present together.+- If there are both a special timestamp and a regular timestamp present, the+  regular timestamp must appear on the next line (this is how Emacs itself+  inserts them). -## Unimplemented+## Specification Coverage -The following don't parse as of yet:+Basic styling: -- Table Formulas-- Anything involving the TODO system or clocking-in-- `:PROPERTIES:`+| Feature               | Parses? |+|-----------------------|---------|+| Bold / Italics / etc. | Yes     |+| URLs / Images         | Yes     |+| Lists                 | Yes     |++Headings:++| Feature      | Parses? |+|--------------|---------|+| Nesting      | Yes     |+| `TODO`       | Yes     |+| Priorities   | Yes     |+| Tags         | Yes     |+| Timestamps   | Yes     |+| `PROPERTIES` | Yes     |++Tables:++| Feature    | Parses? |+|------------|---------|+| Basic      | Yes     |+| Formulas   | **No**  |+| Properties | **No**  |++Blocks:++| Feature     | Parses? |+|-------------|---------|+| Quotes      | Yes     |+| Examples    | Yes     |+| Source Code | Yes     |+| Center      | **No**  |+| Comment     | **No**  |+| Export      | **No**  |+| Verse       | **No**  |
lib/Data/Org.hs view
@@ -5,7 +5,7 @@  -- | -- Module    : Data.Org--- Copyright : (c) Colin Woodbury, 2020+-- Copyright : (c) Colin Woodbury, 2020 - 2021 -- License   : BSD3 -- Maintainer: Colin Woodbury <colin@fosskers.ca> --@@ -15,11 +15,26 @@  module Data.Org   ( -- * Types+    -- ** Top-level     OrgFile(..)   , emptyOrgFile   , OrgDoc(..)   , emptyDoc+  , allDocTags+    -- ** Timestamps+  , OrgDateTime(..)+  , OrgTime(..)+  , Repeater(..)+  , RepeatMode(..)+  , Delay(..)+  , DelayMode(..)+  , Interval(..)+    -- ** Markup   , Section(..)+  , titled+  , allSectionTags+  , Todo(..)+  , Priority(..)   , Block(..)   , Words(..)   , ListItems(..)@@ -36,10 +51,16 @@   , meta   , orgP   , section+  , properties+  , property   , paragraph   , table   , list   , line+  , timestamp+  , date+  , timeRange+  , repeater     -- * Pretty Printing   , prettyOrgFile   , prettyOrg@@ -50,19 +71,25 @@ import           Control.Monad (void, when) import           Data.Bool (bool) import           Data.Functor (($>))-import           Data.Hashable (Hashable(..))+import           Data.Hashable (Hashable) import           Data.List.NonEmpty (NonEmpty(..)) import qualified Data.List.NonEmpty as NEL import qualified Data.Map.Strict as M+import           Data.Maybe (catMaybes, fromMaybe) import           Data.Semigroup (sconcat)+import qualified Data.Set as S import           Data.Text (Text) import qualified Data.Text as T+import           Data.Time (Day, TimeOfDay(..), fromGregorian, showGregorian)+import           Data.Time.Calendar (DayOfWeek(..)) import           Data.Void (Void) import           GHC.Generics (Generic) import           System.FilePath (takeExtension) import           Text.Megaparsec hiding (sepBy1, sepEndBy1, some, someTill) import           Text.Megaparsec.Char+import           Text.Megaparsec.Char.Lexer (decimal) import qualified Text.Megaparsec.Char.Lexer as L+import           Text.Printf (printf)  -------------------------------------------------------------------------------- -- Types@@ -78,7 +105,7 @@   -- #+AUTHOR: Colin   -- @   , orgDoc  :: OrgDoc }-  deriving stock (Eq, Show, Generic)+  deriving stock (Eq, Ord, Show, Generic)  emptyOrgFile :: OrgFile emptyOrgFile = OrgFile mempty emptyDoc@@ -96,12 +123,21 @@ data OrgDoc = OrgDoc   { docBlocks   :: [Block]   , docSections :: [Section] }-  deriving stock (Eq, Show, Generic)-  deriving anyclass (Hashable)+  deriving stock (Eq, Ord, Show, Generic)  emptyDoc :: OrgDoc emptyDoc = OrgDoc [] [] +-- | All unique section tags in the entire document.+--+-- Section tags appear on the same row as a header title, but right-aligned.+--+-- @+-- * This is a Heading                :tag1:tag2:+-- @+allDocTags :: OrgDoc -> S.Set Text+allDocTags = foldMap allSectionTags . docSections+ -- | Some logically distinct block of Org content. data Block   = Quote Text@@ -110,9 +146,96 @@   | List ListItems   | Table (NonEmpty Row)   | Paragraph (NonEmpty Words)-  deriving stock (Eq, Show, Generic)-  deriving anyclass (Hashable)+  deriving stock (Eq, Ord, Show, Generic) +-- | An org-mode timestamp. Must contain at least a year-month-day and the day+-- of the week:+--+-- @+-- \<2021-04-27 Tue\>+-- @+--+-- but also may contain a time:+--+-- @+-- \<2021-04-27 Tue 12:00\>+-- @+--+-- or a time range:+--+-- @+-- \<2021-04-27 Tue 12:00-13:00\>+-- @+--+-- and/or a repeater value:+--+-- @+-- \<2021-04-27 Tue +1w\>+-- @+data OrgDateTime = OrgDateTime+  { dateDay       :: Day+  , dateDayOfWeek :: DayOfWeek+  , dateTime      :: Maybe OrgTime+  , dateRepeat    :: Maybe Repeater+  , dateDelay     :: Maybe Delay }+  deriving stock (Eq, Show)++-- | A lack of a specific `OrgTime` is assumed to mean @00:00@, the earliest+-- possible time for that day.+instance Ord OrgDateTime where+  compare (OrgDateTime d0 _ mt0 _ _) (OrgDateTime d1 _ mt1 _ _) = case compare d0 d1 of+    LT -> LT+    GT -> GT+    EQ -> case (mt0, mt1) of+      (Nothing, Nothing) -> EQ+      (Just _, Nothing)  -> GT+      (Nothing, Just _)  -> LT+      (Just t0, Just t1) -> compare t0 t1++-- | The time portion of the full timestamp. May be a range, as seen in the+-- following full timestamp:+--+-- @+-- \<2021-04-27 Tue 12:00-13:00\>+-- @+data OrgTime = OrgTime+  { timeStart :: TimeOfDay+  , timeEnd   :: Maybe TimeOfDay }+  deriving stock (Eq, Ord, Show)++-- | An indication of how often a timestamp should be automatically reapplied in+-- the Org Agenda.+data Repeater = Repeater+  { repMode     :: RepeatMode+  , repValue    :: Word+  , repInterval :: Interval }+  deriving stock (Eq, Ord, Show)++-- | The nature of the repitition.+data RepeatMode+  = Single     -- ^ Apply the interval value to the original timestamp once: @+@+  | Jump       -- ^ Apply the interval value as many times as necessary to arrive on a future date: @++@+  | FromToday  -- ^ Apply the interval value from today: @.+@+  deriving stock (Eq, Ord, Show)++-- | The timestamp repitition unit.+data Interval = Hour | Day | Week | Month | Year+  deriving stock (Eq, Ord, Show)++-- | Delay the appearance of a timestamp in the agenda.+data Delay = Delay+  { delayMode     :: DelayMode+  , delayValue    :: Word+  , delayInterval :: Interval }+  deriving stock (Eq, Ord, Show)++-- | When a repeater is also present, should the delay be for the first value or+-- all of them?+data DelayMode+  = DelayOne  -- ^ As in: @--2d@+  | DelayAll  -- ^ As in: @-2d@+  deriving stock (Eq, Ord, Show)+ -- | A subsection, marked by a heading line and followed recursively by an -- `OrgDoc`. --@@ -122,12 +245,40 @@ -- This is content in the sub ~OrgDoc~. -- @ data Section = Section-  { sectionHeading :: NonEmpty Words-  , sectionTags    :: [Text]-  , sectionDoc     :: OrgDoc }-  deriving stock (Eq, Show, Generic)-  deriving anyclass (Hashable)+  { sectionTodo      :: Maybe Todo+  , sectionPriority  :: Maybe Priority+  , sectionHeading   :: NonEmpty Words+  , sectionTags      :: [Text]+  , sectionClosed    :: Maybe OrgDateTime+  , sectionDeadline  :: Maybe OrgDateTime+  , sectionScheduled :: Maybe OrgDateTime+  , sectionTimestamp :: Maybe OrgDateTime+    -- ^ A timestamp for general events that are neither a DEADLINE nor SCHEDULED.+  , sectionProps     :: M.Map Text Text+  , sectionDoc       :: OrgDoc }+  deriving stock (Eq, Ord, Show, Generic) +-- | A mostly empty invoking of a `Section`.+titled :: Words -> Section+titled ws = Section Nothing Nothing (ws:|[]) [] Nothing Nothing Nothing Nothing mempty emptyDoc++-- | All unique tags with a section and its subsections.+allSectionTags :: Section -> S.Set Text+allSectionTags (Section _ _ _ sts _ _ _ _ _ doc) = S.fromList sts <> allDocTags doc++-- | The completion state of a heading that is considered a "todo" item.+data Todo = TODO | DONE+  deriving stock (Eq, Ord, Show, Generic)++-- | A priority value, usually associated with a @TODO@ marking, as in:+--+-- @+-- *** TODO [#A] Cure cancer with Haskell+-- *** TODO [#B] Eat lunch+-- @+newtype Priority = Priority { priority :: Text }+  deriving stock (Eq, Ord, Show, Generic)+ -- | An org list constructed of @-@ characters. -- -- @@@ -140,13 +291,11 @@ -- - Feed the elephant -- @ newtype ListItems = ListItems (NonEmpty Item)-  deriving stock (Eq, Show, Generic)-  deriving anyclass (Hashable)+  deriving stock (Eq, Ord, Show, Generic)  -- | A line in a bullet-list. Can contain sublists, as shown in `ListItems`. data Item = Item (NonEmpty Words) (Maybe ListItems)-  deriving stock (Eq, Show, Generic)-  deriving anyclass (Hashable)+  deriving stock (Eq, Ord, Show, Generic)  -- | A row in an org table. Can have content or be a horizontal rule. --@@ -156,13 +305,11 @@ -- | D | E | F | -- @ data Row = Break | Row (NonEmpty Column)-  deriving stock (Eq, Show, Generic)-  deriving anyclass (Hashable)+  deriving stock (Eq, Ord, Show, Generic)  -- | A possibly empty column in an org table. data Column = Empty | Column (NonEmpty Words)-  deriving stock (Eq, Show, Generic)-  deriving anyclass (Hashable)+  deriving stock (Eq, Ord, Show, Generic)  -- | The fundamental unit of Org text content. `Plain` units are split -- word-by-word.@@ -175,21 +322,19 @@   | Strike Text   | Link URL (Maybe Text)   | Image URL-  | Tags (NonEmpty Text)   | Punct Char   | Plain Text-  deriving stock (Eq, Show, Generic)+  deriving stock (Eq, Ord, Show, Generic)   deriving anyclass (Hashable)  -- | The url portion of a link. newtype URL = URL Text-  deriving stock (Eq, Show, Generic)+  deriving stock (Eq, Ord, Show, Generic)   deriving anyclass (Hashable)  -- | The programming language some source code block was written in. newtype Language = Language Text-  deriving stock (Eq, Show, Generic)-  deriving anyclass (Hashable)+  deriving stock (Eq, Ord, Show, Generic)  -------------------------------------------------------------------------------- -- Parser@@ -231,24 +376,117 @@       , try table       , paragraph ]  -- TODO Paragraph needs to fail if it detects a heading. --- | If a line stars with @*@ and a space, it is a `Section` heading.-heading :: Parser (T.Text, NonEmpty Words, [Text])+-- | If a line starts with @*@ and a space, it is a `Section` heading.+heading :: Parser (T.Text, Maybe Todo, Maybe Priority, NonEmpty Words, [Text]) heading = do   stars <- someOf '*' <* char ' '-  ws <- line '\n'-  case nelUnsnoc ws of-    (Tags ts, Just rest) -> pure (stars, rest, NEL.toList ts)-    _                    -> pure (stars, ws, [])+  (mtd, mpr, ws, mts) <- headerLine+  case mts of+    Nothing -> pure (stars, mtd, mpr, ws, [])+    Just ts -> pure (stars, mtd, mpr, ws, NEL.toList ts)  section :: Int -> Parser Section section depth = L.lexeme space $ do-  (stars, ws, ts) <- heading+  (stars, td, pr, ws, ts) <- heading   -- Fail if we've found a parent heading --   when (T.length stars < depth) $ failure Nothing mempty   -- Otherwise continue --+  (cl, dl, sc) <- fromMaybe (Nothing, Nothing, Nothing) <$> optional (try $ newline *> hspace *> timestamps)+  tm <- optional (try $ newline *> hspace *> stamp)+  props <- fromMaybe mempty <$> optional (try $ newline *> hspace *> properties)   void space-  Section ws ts <$> orgP' (succ depth)+  Section td pr ws ts cl dl sc tm props <$> orgP' (succ depth) +timestamps :: Parser (Maybe OrgDateTime, Maybe OrgDateTime, Maybe OrgDateTime)+timestamps = do+  mc <- optional closed+  void hspace+  md <- optional deadline+  void hspace+  ms <- optional scheduled+  case (mc, md, ms) of+    (Nothing, Nothing, Nothing) -> failure Nothing mempty+    _                           -> pure (mc, md, ms)++-- | An active timestamp.+stamp :: Parser OrgDateTime+stamp = between (char '<') (char '>') timestamp++closed :: Parser OrgDateTime+closed = string "CLOSED: " *> between (char '[') (char ']') timestamp++deadline :: Parser OrgDateTime+deadline = string "DEADLINE: " *> stamp++scheduled :: Parser OrgDateTime+scheduled = string "SCHEDULED: " *> stamp++timestamp :: Parser OrgDateTime+timestamp = OrgDateTime+  <$> date+  <*> (hspace1 *> dow)+  <*> optional (try $ hspace1 *> timeRange)+  <*> optional (try $ hspace1 *> repeater)+  <*> optional (hspace1 *> delay)++date :: Parser Day+date = fromGregorian <$> decimal <*> (char '-' *> decimal) <*> (char '-' *> decimal)++dow :: Parser DayOfWeek+dow = choice+  [ Monday    <$ string "Mon"+  , Tuesday   <$ string "Tue"+  , Wednesday <$ string "Wed"+  , Thursday  <$ string "Thu"+  , Friday    <$ string "Fri"+  , Saturday  <$ string "Sat"+  , Sunday    <$ string "Sun" ]++timeRange :: Parser OrgTime+timeRange = OrgTime <$> t <*> optional (char '-' *> t)+  where+    t :: Parser TimeOfDay+    t = do+      h <- decimal+      void $ char ':'+      m <- decimal+      s <- optional $ do+        void $ char ':'+        decimal+      pure $ TimeOfDay h m (fromMaybe 0 s)++repeater :: Parser Repeater+repeater = Repeater+  <$> choice [ string ".+" $> FromToday, string "++" $> Jump, char '+' $> Single ]+  <*> decimal+  <*> interval++delay :: Parser Delay+delay = Delay+  <$> choice [ string "--" $> DelayOne, char '-' $> DelayAll ]+  <*> decimal+  <*> interval++interval :: Parser Interval+interval = choice [ char 'h' $> Hour, char 'd' $> Day, char 'w' $> Week, char 'm' $> Month, char 'y' $> Year ]++properties :: Parser (M.Map Text Text)+properties = do+  void $ string ":PROPERTIES:"+  void newline+  void hspace+  ps <- (hspace *> property <* newline <* hspace) `manyTill` string ":END:"+  pure $ M.fromList ps++property :: Parser (Text, Text)+property = do+  void $ char ':'+  key <- someTill' ':' -- TODO Newlines?+  void $ char ':'+  void hspace+  val <- takeWhile1P (Just "Property Value") (/= '\n')+  pure (key, val)+ quote :: Parser Block quote = L.lexeme space $ do   void top <* newline@@ -332,8 +570,18 @@   notFollowedBy heading   Paragraph . sconcat <$> sepEndBy1 (line '\n') newline +headerLine :: Parser (Maybe Todo, Maybe Priority, NonEmpty Words, Maybe (NonEmpty Text))+headerLine = do+  td <- optional . try $ (string "TODO" $> TODO) <|> (string "DONE" $> DONE)+  void hspace+  pr <- optional . try . fmap Priority $ between (char '[') (char ']') (char '#' *> someTill' ']')+  void hspace+  ws <- (wordChunk '\n' <* hspace) `someTill` lookAhead (try $ void tags <|> void (char '\n') <|> eof)+  ts <- optional tags+  pure (td, pr, ws, ts)+ line :: Char -> Parser (NonEmpty Words)-line end = sepEndBy1 (wordChunk end) (manyOf ' ')+line end = wordChunk end `sepEndBy1` manyOf ' '  -- | RULES --@@ -354,20 +602,20 @@   , try $ Strike    <$> between (char '+') (char '+') (someTill' '+') <* pOrS   , try image   , try link-  , try tags   , try $ Punct     <$> oneOf punc   , Plain           <$> takeWhile1P (Just "plain text") (\c -> c /= ' ' && c /= end) ]   where+    -- | Punctuation, space, or the end of the file.     pOrS :: Parser ()     pOrS = lookAhead $ void (oneOf $ end : ' ' : punc) <|> eof  punc :: String punc = ".,!?():;'" -tags :: Parser Words+tags :: Parser (NonEmpty Text) tags = do   void $ char ':'-  Tags <$> (T.pack . NEL.toList <$> some letterChar) `sepEndBy1` char ':'+  (T.pack . NEL.toList <$> some (alphaNumChar <|> char '_' <|> char '@')) `sepEndBy1` char ':'  image :: Parser Words image = between (char '[') (char ']') $@@ -416,17 +664,100 @@   T.intercalate "\n\n" $ map prettyBlock bs <> map (prettySection depth) ss  prettySection :: Int -> Section -> Text-prettySection depth (Section ws ts od) = headig <> "\n\n" <> subdoc+prettySection depth (Section td pr ws ts cl dl sc tm ps od) =+  T.intercalate "\n" $ catMaybes+  [ Just headig+  , stamps+  , time <$> tm+  , props+  , Just subdoc ]   where+    pr' :: Priority -> Text+    pr' (Priority t) = "[#" <> t <> "]"+     -- TODO There is likely a punctuation bug here.+    --+    -- Sun Apr 25 09:59:01 AM PDT 2021: I wish you had elaborated.     headig = T.unwords       $ T.replicate depth "*"-      : NEL.toList (NEL.map prettyWords ws)+      : catMaybes [ T.pack . show <$> td, pr' <$> pr ]+      <> NEL.toList (NEL.map prettyWords ws)       <> bool [":" <> T.intercalate ":" ts <> ":"] [] (null ts) +    indent :: Text+    indent = T.replicate (depth + 1) " "++    -- | The order of "special" timestamps is CLOSED, DEADLINE, then SCHEDULED.+    -- Any permutation of these may appear.+    stamps :: Maybe Text+    stamps = case catMaybes [fmap cl' cl, fmap dl' dl, fmap sc' sc] of+      [] -> Nothing+      xs -> Just $ indent <> T.unwords xs++    cl' :: OrgDateTime -> Text+    cl' x = "CLOSED: [" <> prettyDateTime x <> "]"++    dl' :: OrgDateTime -> Text+    dl' x = "DEADLINE: <" <> prettyDateTime x <> ">"++    sc' :: OrgDateTime -> Text+    sc' x = "SCHEDULED: " <> time x++    time :: OrgDateTime -> Text+    time x = "<" <> prettyDateTime x <> ">"++    props :: Maybe Text+    props+      | null ps = Nothing+      | otherwise = Just . T.intercalate "\n" $ (indent <> ":PROPERTIES:") : items <> [indent <> ":END:"]+      where+        items :: [Text]+        items = map (\(k, v) -> indent <> ":" <> k <> ": " <> v) $ M.toList ps+     subdoc :: Text     subdoc = prettyOrg' (succ depth) od +prettyDateTime :: OrgDateTime -> Text+prettyDateTime (OrgDateTime d w t rep del) =+  T.unwords $ catMaybes [ Just d', Just w', prettyTime <$> t, prettyRepeat <$> rep, prettyDelay <$> del ]+  where+    d' :: Text+    d' = T.pack $ showGregorian d++    w' :: Text+    w' = T.pack . take 3 $ show w++prettyTime :: OrgTime -> Text+prettyTime (OrgTime s me) = tod s <> maybe "" (\e -> "-" <> tod e) me+  where+    tod :: TimeOfDay -> Text+    tod (TimeOfDay h m _) = T.pack $ printf "%02d:%02d" h m++prettyRepeat :: Repeater -> Text+prettyRepeat (Repeater m v i) = m' <> T.pack (show v) <> prettyInterval i+  where+    m' :: Text+    m' = case m of+      Single    -> "+"+      Jump      -> "++"+      FromToday -> ".+"++prettyDelay :: Delay -> Text+prettyDelay (Delay m v i) = m' <> T.pack (show v) <> prettyInterval i+  where+    m' :: Text+    m' = case m of+      DelayOne -> "--"+      DelayAll -> "-"++prettyInterval :: Interval -> Text+prettyInterval i = case i of+  Hour  -> "h"+  Day   -> "d"+  Week  -> "w"+  Month -> "m"+  Year  -> "y"+ prettyBlock :: Block -> Text prettyBlock o = case o of   Code l t -> "#+begin_src" <> maybe "" (\(Language l') -> " " <> l' <> "\n") l@@ -479,9 +810,5 @@   Link (URL url) Nothing  -> "[[" <> url <> "]]"   Link (URL url) (Just t) -> "[[" <> url <> "][" <> t <> "]]"   Image (URL url)         -> "[[" <> url <> "]]"-  Tags ts                 ->  ":" <> T.intercalate ":" (NEL.toList ts) <> ":"   Punct c                 -> T.singleton c   Plain t                 -> t--nelUnsnoc :: NonEmpty a -> (a, Maybe (NonEmpty a))-nelUnsnoc ne = (NEL.last ne, NEL.nonEmpty $ NEL.init ne)
org-mode.cabal view
@@ -1,13 +1,13 @@ cabal-version:      2.2 name:               org-mode-version:            1.1.1+version:            2.0.0 synopsis:           Parser for Emacs org-mode files. description:        Parser for Emacs org-mode files. category:           Data homepage:           https://github.com/fosskers/org-mode author:             Colin Woodbury maintainer:         colin@fosskers.ca-copyright:          2020 Colin Woodbury+copyright:          2020 - 2021 Colin Woodbury license:            BSD-3-Clause license-file:       LICENSE build-type:         Simple@@ -22,8 +22,7 @@   default-extensions: OverloadedStrings   ghc-options:     -Wall -Wpartial-fields -Wincomplete-record-updates-    -Wincomplete-uni-patterns -Widentities-    -funclutter-valid-hole-fits+    -Wincomplete-uni-patterns -Widentities -funclutter-valid-hole-fits    build-depends:     , base        >=4.12 && <5@@ -39,6 +38,7 @@     , filepath     , hashable            >=1.2 && <1.4     , parser-combinators  >=1.1 && <1.3+    , time                >=1.9  test-suite org-mode-test   import:         commons@@ -50,3 +50,4 @@     , org-mode     , tasty        ^>=1.2     , tasty-hunit  ^>=0.10+    , time         >=1.9
test/Test.hs view
@@ -7,6 +7,8 @@ import           Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.IO as T+import           Data.Time.Calendar (DayOfWeek(..), fromGregorian)+import           Data.Time.LocalTime (TimeOfDay(..)) import           Data.Void (Void) import           Test.Tasty import           Test.Tasty.HUnit@@ -27,29 +29,137 @@ suite :: T.Text -> T.Text -> TestTree suite simple full = testGroup "Unit Tests"   [ testGroup "Basic Markup"-    [ testCase "Header" $ parseMaybe (section 1) "* A"-      @?= Just (Section [Plain "A"] [] emptyDoc)+    [ testCase "Header" $ parseMaybe (section 1) "* A" @?= Just (titled (Plain "A"))     , testCase "Header - Subsection" $ parseMaybe (section 1) "* A\n** B"-      @?= Just (Section [Plain "A"] [] (OrgDoc [] [Section [Plain "B"] [] emptyDoc]))+      @?= Just ((titled (Plain "A")) { sectionDoc = OrgDoc [] [titled (Plain "B")] })+     , testCase "Header - Back again"       $ testPretty orgP "Header" "* A\n** B\n* C"-      $ OrgDoc [] [ Section [Plain "A"] [] (OrgDoc [] [Section [Plain "B"] [] emptyDoc])-                  , Section [Plain "C"] [] emptyDoc ]+      $ OrgDoc [] [ (titled (Plain "A")) { sectionDoc = OrgDoc [] [titled (Plain "B")] }, titled (Plain "C") ]+     , testCase "Header - Contents"       $ testPretty orgP "Header" "* A\nD\n\n** B\n* C"  -- TODO Requires an extra newline!       $ OrgDoc []-      [ Section [Plain "A"] [] (OrgDoc [Paragraph [Plain "D"]] [Section [Plain "B"] [] emptyDoc])-      , Section [Plain "C"] [] emptyDoc ]-    , testCase "Header - Tags"+      [ (titled (Plain "A")) { sectionDoc = OrgDoc [Paragraph [Plain "D"]] [ titled (Plain "B") ] }+      , titled (Plain "C") ]++    , testCase "Header - TODO"+      $ testPretty orgP "Header" "* TODO A"+      $ OrgDoc []+      [ (titled (Plain "A")) { sectionTodo = Just TODO }]++    , testCase "Header - Priority"+      $ testPretty orgP "Header" "* [#A] A"+      $ OrgDoc []+      [ (titled (Plain "A")) { sectionPriority = Just $ Priority "A" }]++    , testCase "Header - TODO + Priority"+      $ testPretty orgP "Header" "* TODO [#A] A"+      $ OrgDoc []+      [ (titled (Plain "A")) { sectionTodo = Just TODO, sectionPriority = Just $ Priority "A" }]++    , testCase "Header - One line, single tag"+      $ testPretty orgP "Header" "* A  :this:"+      $ OrgDoc [] [ (titled (Plain "A")) { sectionTags = ["this"] } ]++    , testCase "Header - One line, multiple tags"       $ testPretty orgP "Header" "* A  :this:that:"-      $ OrgDoc [] [Section [Plain "A"] ["this", "that"] emptyDoc]+      $ OrgDoc [] [ (titled (Plain "A")) { sectionTags = ["this", "that"] } ]+     , testCase "Header - More Tags"       $ testPretty orgP "Header" "* A  :this:that:\n** B   :other:\n* C"       $ OrgDoc []-      [ Section [Plain "A"] ["this", "that"] (OrgDoc [] [Section [Plain "B"] ["other"] emptyDoc])-      , Section [Plain "C"] [] emptyDoc-      ]+      [ (titled (Plain "A"))+        { sectionTags = ["this", "that"]+        , sectionDoc = OrgDoc [] [ (titled (Plain "B")) { sectionTags = ["other"] } ] }+      , titled (Plain "C") ] +    , testCase "Header - Non-tag Smiley"+      $ testPretty orgP "Header" "* A :)"+      $ OrgDoc [] [ Section Nothing Nothing [Plain "A", Punct ':', Punct ')'] [] Nothing Nothing Nothing Nothing mempty emptyDoc ]++    , testCase "Header - Vanilla Timestamp"+      $ testPretty orgP "Header" "* A\n  <2021-04-19 Mon>"+      $ let tm = OrgDateTime { dateDay = fromGregorian 2021 4 19+                             , dateDayOfWeek = Monday+                             , dateTime = Nothing+                             , dateRepeat = Nothing+                             , dateDelay = Nothing }+        in OrgDoc [] [ (titled (Plain "A")) { sectionTimestamp = Just tm } ]++    , testCase "Header - CLOSED"+      $ testPretty orgP "Header" "* A\n  CLOSED: [2021-04-19 Mon 15:43]"+      $ let cl = OrgDateTime { dateDay = fromGregorian 2021 4 19+                             , dateDayOfWeek = Monday+                             , dateTime = Just $ OrgTime (TimeOfDay 15 43 0) Nothing+                             , dateRepeat = Nothing+                             , dateDelay = Nothing }+        in OrgDoc [] [ (titled (Plain "A")) { sectionClosed = Just cl } ]++    , testCase "Header - DEADLINE"+      $ testPretty orgP "Header" "* A\n  DEADLINE: <2021-04-19 Mon>"+      $ let dl = OrgDateTime { dateDay = fromGregorian 2021 4 19+                             , dateDayOfWeek = Monday+                             , dateTime = Nothing+                             , dateRepeat = Nothing+                             , dateDelay = Nothing }+        in OrgDoc [] [ (titled (Plain "A")) { sectionDeadline = Just dl } ]++    , testCase "Header - SCHEDULED"+      $ testPretty orgP "Header" "* A\n  SCHEDULED: <2021-04-19 Mon>"+      $ let sc = OrgDateTime { dateDay = fromGregorian 2021 4 19+                             , dateDayOfWeek = Monday+                             , dateTime = Nothing+                             , dateRepeat = Nothing+                             , dateDelay = Nothing }+        in OrgDoc [] [ (titled (Plain "A")) { sectionScheduled = Just sc } ]++    , testCase "Header - CLOSED/DEADLINE"+      $ testPretty orgP "Header" "* A\n  CLOSED: [2021-04-19 Mon 15:43] DEADLINE: <2021-04-19 Mon>"+      $ let dl = OrgDateTime { dateDay = fromGregorian 2021 4 19+                             , dateDayOfWeek = Monday+                             , dateTime = Nothing+                             , dateRepeat = Nothing+                             , dateDelay = Nothing }+            cl = OrgDateTime { dateDay = fromGregorian 2021 4 19+                             , dateDayOfWeek = Monday+                             , dateTime = Just $ OrgTime (TimeOfDay 15 43 0) Nothing+                             , dateRepeat = Nothing+                             , dateDelay = Nothing }+        in OrgDoc [] [ (titled (Plain "A")) { sectionClosed = Just cl, sectionDeadline = Just dl } ]++    , testCase "Header - CLOSED/DEADLINE - More"+      $ testPretty orgP "Header" "* A\n  CLOSED: [2021-04-19 Mon 15:43] DEADLINE: <2021-04-19 Mon>\nD"+      $ let dl = OrgDateTime { dateDay = fromGregorian 2021 4 19+                             , dateDayOfWeek = Monday+                             , dateTime = Nothing+                             , dateRepeat = Nothing+                             , dateDelay = Nothing }+            cl = OrgDateTime { dateDay = fromGregorian 2021 4 19+                             , dateDayOfWeek = Monday+                             , dateTime = Just $ OrgTime (TimeOfDay 15 43 0) Nothing+                             , dateRepeat = Nothing+                             , dateDelay = Nothing }+        in OrgDoc [] [ Section Nothing Nothing [Plain "A"] [] (Just cl) (Just dl) Nothing Nothing mempty (OrgDoc [ Paragraph [Plain "D"]] []) ]++    , testCase "Header - Empty Properties Drawer"+      $ testPretty orgP "Header" "* A\n  :PROPERTIES:\n  :END:"+      $ OrgDoc [] [ titled (Plain "A") ]++    , testCase "Header - One Property"+      $ testPretty orgP "Header" "* A\n  :PROPERTIES:\n  :Cat: Jack\n  :END:\nHi"+      $ OrgDoc [] [ (titled (Plain "A"))+                    { sectionProps = [("Cat", "Jack")]+                    , sectionDoc = OrgDoc [Paragraph [Plain "Hi"]] [] }]++    , testCase "Header - Two Properties"+      $ testPretty orgP "Header" "* A\n  :PROPERTIES:\n  :Cat: Jack\n  :Age: 7\n  :END:"+      $ OrgDoc [] [ (titled (Plain "A")) { sectionProps = [("Cat", "Jack"), ("Age", "7")] } ]++    , testCase "Properties"+      $ testPretty properties "Properties" ":PROPERTIES:\n  :Cat: Jack\n  :END:" [("Cat", "Jack")]+    , testCase "Property" $ testPretty property "Property" ":Cat: Jack" ("Cat", "Jack")+     , testCase "Bold" $ parseMaybe orgP "*Bold*"       @?= Just (OrgDoc [Paragraph [Bold "Bold"]] [])     , testCase "Italics" $ parseMaybe orgP "/Italic/"@@ -100,6 +210,62 @@     , testCase "Line - Dummy markup symbol" $ testPretty (line '\n') "Line" "A ~ B"       [Plain "A", Plain "~", Plain "B"]     ]+  , testGroup "Timestamps"+    [ testCase "Repeater" $ testPretty repeater "Repeater" "+2w" (Repeater Single 2 Week)+    , testCase "Time - Morning" $ testPretty timeRange "Time" "09:30" (OrgTime (TimeOfDay 9 30 0) Nothing)+    , testCase "Time - Afternoon" $ testPretty timeRange "Time" "14:30" (OrgTime (TimeOfDay 14 30 0) Nothing)+    , testCase "Time - Range" $ testPretty timeRange "Time" "09:30-14:30" (OrgTime (TimeOfDay 9 30 0) (Just (TimeOfDay 14 30 0)))+    , testCase "Date" $ testPretty date "Date" "2021-04-27" $ fromGregorian 2021 4 27+    , testCase "Timestamp - No time" $ testPretty timestamp "Stamp" "2021-04-27 Tue"+      $ OrgDateTime+      { dateDay = fromGregorian 2021 4 27+      , dateDayOfWeek = Tuesday+      , dateTime = Nothing+      , dateRepeat = Nothing+      , dateDelay = Nothing }+    , testCase "Timestamp - Time" $ testPretty timestamp "Stamp" "2021-04-27 Tue 09:30"+      $ OrgDateTime+      { dateDay = fromGregorian 2021 4 27+      , dateDayOfWeek = Tuesday+      , dateTime = Just $ OrgTime (TimeOfDay 9 30 0) Nothing+      , dateRepeat = Nothing+      , dateDelay = Nothing }+    , testCase "Timestamp - Repeat Single" $ testPretty timestamp "Stamp" "2021-04-27 Tue +2w"+      $ OrgDateTime+      { dateDay = fromGregorian 2021 4 27+      , dateDayOfWeek = Tuesday+      , dateTime = Nothing+      , dateRepeat = Just $ Repeater Single 2 Week+      , dateDelay = Nothing }+    , testCase "Timestamp - Repeat Jump" $ testPretty timestamp "Stamp" "2021-04-27 Tue ++2w"+      $ OrgDateTime+      { dateDay = fromGregorian 2021 4 27+      , dateDayOfWeek = Tuesday+      , dateTime = Nothing+      , dateRepeat = Just $ Repeater Jump 2 Week+      , dateDelay = Nothing }+    , testCase "Timestamp - Time and Repeat" $ testPretty timestamp "Stamp" "2021-04-27 Tue 09:30 +2w"+      $ OrgDateTime+      { dateDay = fromGregorian 2021 4 27+      , dateDayOfWeek = Tuesday+      , dateTime = Just $ OrgTime (TimeOfDay 9 30 0) Nothing+      , dateRepeat = Just $ Repeater Single 2 Week+      , dateDelay = Nothing }+    , testCase "Timestamp - Delay" $ testPretty timestamp "Stamp" "2021-04-27 Tue -3d"+      $ OrgDateTime+      { dateDay = fromGregorian 2021 4 27+      , dateDayOfWeek = Tuesday+      , dateTime = Nothing+      , dateRepeat = Nothing+      , dateDelay = Just $ Delay DelayAll 3 Day }+    , testCase "Timestamp - Repeat and Delay" $ testPretty timestamp "Stamp" "2021-04-27 Tue +2w -3d"+      $ OrgDateTime+      { dateDay = fromGregorian 2021 4 27+      , dateDayOfWeek = Tuesday+      , dateTime = Nothing+      , dateRepeat = Just $ Repeater Single 2 Week+      , dateDelay = Just $ Delay DelayAll 3 Day }+    ]   , testGroup "Composite Structures"     [ testCase "Example" $ parseMaybe orgP "#+begin_example\nHi!\n\nHo\n#+end_example"       @?= Just (OrgDoc [Example "Hi!\n\nHo"] [])@@ -192,6 +358,8 @@         Right r -> case parse orgFile "test.org - reparse" (prettyOrgFile r) of           Left eb' -> assertFailure $ errorBundlePretty eb'           Right r' -> r' @?= r+    , testCase "Full: Tag Extraction"+      $ (allDocTags . orgDoc <$> parse orgFile "test.org" full) @?= Right ["tag1", "tag2", "tag3"]     ]   , testGroup "Megaparsec Sanity"     [ testCase "sepEndBy1" $ testPretty sepTest "sepBy1" "A.A.A.B" ['A', 'A', 'A']
test/simple.org view
@@ -11,5 +11,8 @@ Text!  * Heading 3+  :PROPERTIES:+  :Yes: Fun+  :END:  More text!
test/test.org view
@@ -5,11 +5,11 @@  Some text before the initial heading. -* A Heading+* A Heading                                                            :tag1: -** A subheading+** A subheading                                                        :tag2: -*** We have to go deeper+*** We have to go deeper                                               :tag3:  *This is some bold text* within a normal paragraph. However I should tell you that the paragraph doesn't end there. It has multiple lines! Oh, but having only@@ -23,7 +23,7 @@   main = putStrLn "org-mode tests" #+end_src -** More+** More                                                                :tag2:  And a quote. @@ -32,6 +32,23 @@  - Albert Einstein #+end_quote++** Not a tag! :)++** DONE Gotta do it+   CLOSED: [2021-04-28 Wed 15:10] DEADLINE: <2021-04-29 Thu> SCHEDULED: <2021-04-28 Wed>+   <2021-04-25 Sun>++Stuff.++** TODO [#A] Laundry+   SCHEDULED: <2021-04-30 Fri 13:00 .+1w -1d>++** Props+   <2021-04-28 Wed>+   :PROPERTIES:+   :Yes: Fun+   :END:  * Another Heading!