├── shell.nix ├── Setup.hs ├── .editorconfig ├── test ├── docs │ ├── subtree-list-items.org │ ├── bold-mark-corner-cases.org │ ├── test-document.org │ └── complex-list.org ├── Test.hs ├── Util.hs ├── list-locale.cpp ├── Util │ └── Builder.hs ├── Drawer.hs ├── Content │ ├── List.hs │ ├── Paragraph.hs │ └── Contents.hs ├── Headline.hs ├── Timestamps.hs ├── Document.hs └── Weekdays.hs ├── stack.yaml ├── stack-8.0.1.yaml ├── .gitignore ├── nix ├── 17_09.nix └── fetchNixpkgs.nix ├── src └── Data │ └── OrgMode │ ├── Parse │ └── Attoparsec │ │ ├── Constants.hs │ │ ├── Drawer.hs │ │ ├── Content │ │ ├── Paragraph.hs │ │ ├── List.hs │ │ └── Markup.hs │ │ ├── Drawer │ │ ├── Logbook.hs │ │ ├── Generic.hs │ │ └── Property.hs │ │ ├── Content.hs │ │ ├── Document.hs │ │ ├── Section.hs │ │ ├── Util.hs │ │ ├── Util │ │ └── ParseLinesTill.hs │ │ ├── Headline.hs │ │ └── Time.hs │ ├── Parse.hs │ └── Types.hs ├── default.nix ├── release.nix ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── haddock2hackage └── orgmode-parse.cabal /shell.nix: -------------------------------------------------------------------------------- 1 | (import ./release.nix { }).orgmode-parse.env 2 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.hs] 2 | indent_style = space 3 | indent_size = 2 -------------------------------------------------------------------------------- /test/docs/subtree-list-items.org: -------------------------------------------------------------------------------- 1 | * Header1 2 | ** Header2 3 | *** Header3 4 | :PROPERTIES: 5 | :ONE: two 6 | :END: 7 | * Item1 8 | * Item2 9 | ** Header4 10 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | flags: {} 2 | nix: 3 | packages: 4 | - cabal-install 5 | extra-package-dbs: [] 6 | packages: 7 | - '.' 8 | extra-deps: 9 | - thyme-0.3.5.5 10 | resolver: lts-12.18 11 | -------------------------------------------------------------------------------- /stack-8.0.1.yaml: -------------------------------------------------------------------------------- 1 | flags: {} 2 | nix: 3 | packages: 4 | - cabal-install 5 | extra-package-dbs: [] 6 | packages: 7 | - '.' 8 | extra-deps: 9 | - aeson-1.1.0.0 10 | - insert-ordered-containers-0.2.1.0 11 | resolver: lts-7.24 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .vagrant 3 | *.egg-info 4 | \#* 5 | .#*dist 6 | .#* 7 | cabal-dev 8 | dist 9 | *.o 10 | *.hi 11 | *.chi 12 | *.chs.h 13 | .virthualenv 14 | .hsenv 15 | .cabal-sandbox/ 16 | cabal.sandbox.config 17 | log/ 18 | node_modules 19 | TAGS 20 | .stack-work 21 | tmp/ 22 | -------------------------------------------------------------------------------- /nix/17_09.nix: -------------------------------------------------------------------------------- 1 | let 2 | fetchNixpkgs = import ./fetchNixpkgs.nix; 3 | 4 | in 5 | fetchNixpkgs { 6 | rev = "3389f23412877913b9d22a58dfb241684653d7e9"; 7 | sha256 = "1zf05a90d29bpl7j56y20r3kmrl4xkvg7gsfi55n6bb2r0xp2ma5"; 8 | outputSha256 = "0wgm7sk9fca38a50hrsqwz6q79z35gqgb9nw80xz7pfdr4jy9pf8"; 9 | } 10 | -------------------------------------------------------------------------------- /test/docs/bold-mark-corner-cases.org: -------------------------------------------------------------------------------- 1 | * corner cases related to mark * 2 | *is not a headline, due to the missing of space after * 3 | 4 | *is a bold content* 5 | 6 | *is not a bold content, due to the space before * 7 | * is an item 8 | and content here belongs to item1. 9 | *is not an item because of the missing spaces 10 | 11 | *is bolded but not an item* 12 | -------------------------------------------------------------------------------- /test/docs/test-document.org: -------------------------------------------------------------------------------- 1 | * TODO [#A] <2017-08-22 13:22-20:00> Test the parsing of an entire document from file :TESTING:DOC: 2 | :PROPERTIES: 3 | :DATE: [2015-08-02 Sun] 4 | :END: 5 | 6 | This should be parsed by the document parser combinator. 7 | 8 | Another line 9 | 10 | ** A sub-heading 11 | <2017-08-22 13:22-20:00> 12 | :LOGBOOK: 13 | CLOCK: [2015-10-05 Mon 17:13]--[2015-10-05 Mon 17:14] => 0:01 14 | :END: 15 | 16 | :CUSTOMDRAWER: 17 | with arbitrayr text 18 | :END: 19 | 20 | :ANOTHERDRAWER: 21 | with more 12121 text 22 | 23 | boolkjsdf 24 | 25 | asdklfjskldjf 26 | :END: 27 | 28 | 29 | extra section *text* 30 | -------------------------------------------------------------------------------- /test/Test.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Content.Contents 4 | import Content.List 5 | import Content.Paragraph 6 | import Document 7 | import Drawer 8 | import Headline 9 | import Test.Tasty 10 | import Timestamps 11 | 12 | main :: IO () 13 | main = defaultMain tests 14 | 15 | tests :: TestTree 16 | tests = testGroup 17 | "OrgMode Parser Tests" 18 | [ parserHeadlineTests 19 | , parserDrawerTests 20 | , parserTimestampTests 21 | , parserParagraphs 22 | , parserLists 23 | , parserContents 24 | , parserWeekdayTests 25 | , parserSmallDocumentTests 26 | ] 27 | -------------------------------------------------------------------------------- /test/docs/complex-list.org: -------------------------------------------------------------------------------- 1 | * Just a Headline 2 | ** Lord of the Rings 3 | My favorite scenes are (in this order) 4 | 1. The attack of the Rohirrim 5 | 2. Eowyn's fight with the witch king 6 | + this was already my favorite scene in the book 7 | + I really like Miranda Otto. 8 | 3. Peter Jackson being shot by Legolas 9 | - on DVD only 10 | He makes a really funny face when it happens. 11 | But in the end, no individual scenes matter but the film as a whole. 12 | Important actors in this film are: 13 | - Elijah Wood :: He plays Frodo 14 | - Sean Astin :: He plays Sam, Frodo's friend. I still remember 15 | him very well from his role as Mikey Walsh in The Goonies. 16 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Constants.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Data.OrgMode.Parse.Attoparsec.Constants 4 | -- Copyright : © 2014 Parnell Springmeyer 5 | -- License : All Rights Reserved 6 | -- Maintainer : Parnell Springmeyer 7 | -- Stability : stable 8 | -- 9 | -- Constants for some default values. 10 | ---------------------------------------------------------------------------- 11 | 12 | {-# LANGUAGE OverloadedStrings #-} 13 | 14 | module Data.OrgMode.Parse.Attoparsec.Constants 15 | ( 16 | keywords 17 | ) 18 | where 19 | 20 | import Data.String (IsString) 21 | 22 | keywords :: IsString a => [a] 23 | keywords = [ "TODO", "CANCELED", "DONE" ] 24 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { mkDerivation, aeson, attoparsec, base, bytestring, containers 2 | , free, hashable, HUnit, neat-interpolation, old-locale, semigroups 3 | , stdenv, tasty, tasty-hunit, text, thyme, unordered-containers 4 | }: 5 | mkDerivation { 6 | pname = "orgmode-parse"; 7 | version = "0.2.2"; 8 | src = ./.; 9 | libraryHaskellDepends = [ 10 | aeson attoparsec base bytestring containers free hashable 11 | old-locale semigroups text thyme unordered-containers 12 | ]; 13 | testHaskellDepends = [ 14 | aeson attoparsec base bytestring containers free hashable HUnit 15 | neat-interpolation old-locale semigroups tasty tasty-hunit text 16 | thyme unordered-containers 17 | ]; 18 | description = "A collection of Attoparsec combinators for parsing org-mode flavored documents"; 19 | license = stdenv.lib.licenses.bsd3; 20 | } 21 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Drawer.hs: -------------------------------------------------------------------------------- 1 | {-| 2 | Module : Data.OrgMode.Parse.Attoparsec.Drawer 3 | Copyright : © 2017 Parnell Springmeyer 4 | License : All Rights Reserved 5 | Maintainer : Parnell Springmeyer 6 | Stability : stable 7 | 8 | Attoparsec combinators for parsing drawers in org-mode documents. 9 | -} 10 | 11 | module Data.OrgMode.Parse.Attoparsec.Drawer ( 12 | 13 | -- * Parse PROPERTY drawers 14 | module Data.OrgMode.Parse.Attoparsec.Drawer.Property 15 | 16 | -- * Parse LOGBOOK drawers 17 | , module Data.OrgMode.Parse.Attoparsec.Drawer.Logbook 18 | 19 | -- * Parse generic drawers 20 | , module Data.OrgMode.Parse.Attoparsec.Drawer.Generic 21 | 22 | ) where 23 | 24 | import Data.OrgMode.Parse.Attoparsec.Drawer.Generic 25 | import Data.OrgMode.Parse.Attoparsec.Drawer.Logbook 26 | import Data.OrgMode.Parse.Attoparsec.Drawer.Property 27 | -------------------------------------------------------------------------------- /release.nix: -------------------------------------------------------------------------------- 1 | { compiler ? "ghc844" }: 2 | 3 | let 4 | fetchNixpkgs = import ./nix/fetchNixpkgs.nix; 5 | 6 | nixpkgs = fetchNixpkgs { 7 | rev = "bedbba61380a4da0318de41fcb790c176e1f26d1"; 8 | sha256 = "0z4fgh15nz86kxib9ildmh49v6jim6vgbjyla7jbmgdcl0vd9qsg"; 9 | outputSha256 = "0dxxw2ipa9403nk8lggjsypbr1a9jpb3q4hkjsg89gr5wz26p217"; 10 | }; 11 | 12 | overlays = [ 13 | (newPkgs: oldPkgs: { 14 | haskell = oldPkgs.haskell // { 15 | packages = oldPkgs.haskell.packages // { 16 | "${compiler}" = oldPkgs.haskell.packages."${compiler}".override { 17 | overrides = (haskellPackagesNew: haskellPackagesOld: { 18 | orgmode-parse = haskellPackagesNew.callCabal2nix "orgmode-parse" ./. { }; 19 | }); 20 | }; 21 | }; 22 | }; 23 | }) 24 | ]; 25 | 26 | pkgs = import nixpkgs { inherit overlays; }; 27 | 28 | in 29 | 30 | { orgmode-parse = pkgs.haskell.packages."${compiler}".orgmode-parse;} 31 | -------------------------------------------------------------------------------- /test/Util.hs: -------------------------------------------------------------------------------- 1 | module Util where 2 | 3 | import Data.Attoparsec.Text (Parser, parseOnly) 4 | import Data.Either 5 | import Data.Text (pack, Text) 6 | import Test.HUnit 7 | 8 | testParser :: Parser a -> String -> Assertion 9 | testParser f v = fromEither (parseOnly f $ pack v) 10 | 11 | expectParse :: (Eq a, Show a) => Parser a -- ^ Parser under test 12 | -> Text -- ^ Message under test 13 | -> Either String a -- ^ Expected parse result 14 | -> Assertion 15 | expectParse p t (Left _) = assertBool "Expected parse failure" 16 | (isLeft (parseOnly p t)) 17 | expectParse p t a = assertBool msg (r == a) 18 | where r = parseOnly p t 19 | msg = Prelude.unwords 20 | ["Expected parse to", show a, ". Got", show r] 21 | 22 | fromEither :: Either String a -> Assertion 23 | fromEither (Left e) = assertBool e False 24 | fromEither (Right _) = assertBool "" True 25 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Content/Paragraph.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Data.OrgMode.Parse.Attoparsec.Content.Paragraph 4 | -- Copyright : © 2014 Parnell Springmeyer 5 | -- License : All Rights Reserved 6 | -- Maintainer : Parnell Springmeyer 7 | -- Stability : stable 8 | -- 9 | -- Parsing combinators for org-mode markup and paragraphs. 10 | ---------------------------------------------------------------------------- 11 | 12 | module Data.OrgMode.Parse.Attoparsec.Content.Paragraph 13 | ( 14 | parseParagraph 15 | ) 16 | where 17 | 18 | import Data.Attoparsec.Text (Parser) 19 | 20 | import Data.OrgMode.Parse.Attoparsec.Content.Markup (parseMarkupContent) 21 | import Data.OrgMode.Types (Content (..)) 22 | 23 | -- | If a chunk of text cannot be parsed as other blocks, parse the chunk of text as a paragraph 24 | parseParagraph :: Parser Content 25 | parseParagraph = Paragraph <$> parseMarkupContent 26 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse.hs: -------------------------------------------------------------------------------- 1 | {-| 2 | Module : Data.OrgMode.Parse 3 | Copyright : © 2014 Parnell Springmeyer 4 | License : All Rights Reserved 5 | Maintainer : Parnell Springmeyer 6 | Stability : stable 7 | 8 | Attoparsec combinators for parsing org-mode documents. 9 | -} 10 | 11 | module Data.OrgMode.Parse ( 12 | 13 | -- * Parse OrgMode documents 14 | module Data.OrgMode.Parse.Attoparsec.Document 15 | 16 | -- * Parse headlines 17 | , module Data.OrgMode.Parse.Attoparsec.Headline 18 | 19 | -- * Parse headline metadata sections 20 | , module Data.OrgMode.Parse.Attoparsec.Section 21 | 22 | -- * Parse drawers 23 | , module Data.OrgMode.Parse.Attoparsec.Drawer 24 | 25 | -- * Parse metadata timestamps and modifiers 26 | , module Data.OrgMode.Parse.Attoparsec.Time 27 | ) where 28 | 29 | import Data.OrgMode.Parse.Attoparsec.Document 30 | import Data.OrgMode.Parse.Attoparsec.Drawer 31 | import Data.OrgMode.Parse.Attoparsec.Headline 32 | import Data.OrgMode.Parse.Attoparsec.Section 33 | import Data.OrgMode.Parse.Attoparsec.Time 34 | -------------------------------------------------------------------------------- /test/list-locale.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | A script to generate the list of locales. Before running this script you need to install the locales. 3 | 4 | On Debian do the following: 5 | > apt-get install locales 6 | > edit /etc/locales.gen # comment out all the locales. 7 | > sudo locale-gen 8 | 9 | On other systems please search how to install the locales. 10 | */ 11 | 12 | 13 | #include 14 | #include 15 | #include "locale.h" 16 | #include 17 | 18 | using namespace std; 19 | 20 | int main () { 21 | ifstream ifs("/usr/share/i18n/SUPPORTED"); 22 | while (ifs) { 23 | string locale, lang; 24 | ifs >> locale >> lang; 25 | if (lang=="UTF-8") { 26 | cout << " (\"" << locale << "\", [" ; 27 | char *res; 28 | res = setlocale(LC_TIME, locale.c_str()); 29 | if(!res) cerr << "FAIL" << endl; 30 | char buf[512]; 31 | struct tm *tmp; 32 | time_t day = 86400; 33 | for(time_t t = 0; t < 7*day; t+=day){ 34 | tmp = localtime(&t); 35 | strftime(buf, 512, "\"%a\"", tmp); 36 | if(t>0) cout << ", "; 37 | cout << buf; 38 | } 39 | cout << "])," << endl; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/Util/Builder.hs: -------------------------------------------------------------------------------- 1 | module Util.Builder 2 | ( ListBuilder(..) 3 | , ItemBuilder(..) 4 | , ContentBuilder(..) 5 | ) 6 | where 7 | 8 | import Data.OrgMode.Types (Content (..), Item (..), MarkupText (..)) 9 | import Data.Text (Text) 10 | 11 | 12 | class ListBuilder m where 13 | toL :: ([Item] -> Content) -> m -> Content 14 | 15 | instance ListBuilder Text where 16 | toL l = toL l . Plain 17 | 18 | instance ListBuilder MarkupText where 19 | toL l = l . (:[]) . Item . (:[]) . Paragraph . (:[]) 20 | 21 | instance ListBuilder Content where 22 | toL l x = case x of 23 | UnorderedList _ -> x 24 | OrderedList _ -> x 25 | _ -> l [Item [x]] 26 | 27 | class ItemBuilder m where 28 | toI :: m -> Item 29 | 30 | instance ItemBuilder Text where 31 | toI = Item . (:[]) . Paragraph . (:[]) . Plain 32 | 33 | class ContentBuilder a where 34 | toP :: a -> Content 35 | mark :: ([MarkupText] -> MarkupText) -> a -> Content 36 | 37 | instance ContentBuilder Text where 38 | toP = toP . Plain 39 | mark m = mark m . Plain 40 | 41 | instance ContentBuilder MarkupText where 42 | toP = Paragraph . (:[]) 43 | mark m = Paragraph . (:[]) . m . (:[]) 44 | 45 | instance ContentBuilder Content where 46 | toP = id 47 | mark _ = id 48 | -------------------------------------------------------------------------------- /test/Drawer.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE RankNTypes #-} 3 | 4 | module Drawer where 5 | 6 | import Data.OrgMode.Parse 7 | import Test.Tasty 8 | import Test.Tasty.HUnit 9 | 10 | import Util 11 | 12 | parserDrawerTests :: TestTree 13 | parserDrawerTests = testGroup "Attoparsec PropertyDrawer" 14 | [ testCase "Parse a :PROPERTY: drawer" 15 | testPropertyDrawer 16 | 17 | , testCase "Parse empty :PROPERTY: drawer" 18 | (testParser parseProperties ":PROPERTIES:\n:END:\n") 19 | 20 | , testCase "Parse a :LOGBOOK: drawer" 21 | testLogbookDrawer 22 | 23 | , testCase "Parse a user-defined drawer" 24 | testGenericDrawer 25 | ] 26 | 27 | testPropertyDrawer :: Assertion 28 | testPropertyDrawer = 29 | testParser parseProperties ":PROPERTIES:\n :URL: http://someurl.com?query\n :notes: you should be taking them\n:END:\n" 30 | 31 | testLogbookDrawer :: Assertion 32 | testLogbookDrawer = 33 | testParser parseLogbook ":LOGBOOK:\n CLOCK: [2015-10-05 Mon 17:13] \n CLOCK: [2015-10-05 Mon 17:13]--[2015-10-05 Mon 17:14] => 0:01\n:END:\n" 34 | 35 | testGenericDrawer :: Assertion 36 | testGenericDrawer = 37 | testParser parseDrawer ":MYDRAWER:\n whatever I want can go in here, technically... \n:END:\n" 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | sudo: false 3 | 4 | # Caching so the next build will be fast too. 5 | cache: 6 | directories: 7 | - $HOME/.ghc 8 | - $HOME/.cabal 9 | - $HOME/.stack 10 | - $TRAVIS_BUILD_DIR/.stack-work 11 | 12 | 13 | before_install: 14 | # Download and unpack the stack executable 15 | - mkdir -p ~/.local/bin 16 | - export PATH=$HOME/.local/bin:$PATH 17 | - travis_retry curl -L https://get.haskellstack.org/stable/linux-x86_64.tar.gz | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' 18 | 19 | matrix: 20 | include: 21 | - env: BUILD=stack GHCVER=8.0.1 STACK_YAML=stack-8.0.1.yaml 22 | compiler: ": #stack 8.0.1" 23 | addons: 24 | apt: 25 | sources: 26 | - hvr-ghc 27 | packages: 28 | - ghc-8.0.1 29 | - env: BUILD=stack GHCVER=8.4.3 STACK_YAML=stack.yaml 30 | compiler: ": #stack 8.4.3" 31 | addons: 32 | apt: 33 | sources: 34 | - hvr-ghc 35 | packages: 36 | - ghc-8.4.3 37 | - env: LINT=hlint 38 | 39 | script: 40 | - | 41 | set -ex 42 | if [[ $LINT == 'hlint' ]]; then 43 | curl -sL https://raw.github.com/ndmitchell/hlint/master/misc/travis.sh | sh -s . 44 | else 45 | stack setup 46 | stack --no-terminal test 47 | fi 48 | set +ex 49 | -------------------------------------------------------------------------------- /test/Content/List.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Content.List ( 4 | parserLists 5 | ) where 6 | import Test.Tasty 7 | import Test.Tasty.HUnit 8 | import Data.OrgMode.Types (Content (..), Item (..)) 9 | import Data.OrgMode.Parse.Attoparsec.Content.List (parseList) 10 | import qualified Data.Text as Text 11 | import Data.Text (pack) 12 | import Util 13 | import Util.Builder 14 | 15 | parserLists :: TestTree 16 | parserLists = testGroup "Attoparsec orgmode Paragraph" 17 | [ testCase "Parses a Simple Item" $ 18 | testDocS [" * text "] $ toL UnorderedList (pack "text"), 19 | testCase "Parses a items with line break" $ 20 | testDocS [" * item1 ", " poi"] $ toL UnorderedList (pack "item1 poi"), 21 | testCase "Parses multi items" $ 22 | testDocS [" * item1 ", " * item2 "] $ UnorderedList $ map toI [pack "item1", pack "item2"], 23 | testCase "Parses multi items with child list" $ 24 | testDocS [" * item1 ", " * child1", " * item2 "] $ UnorderedList 25 | [ 26 | Item [toP (pack "item1"), UnorderedList [toI (pack "child1")]], 27 | toI (pack "item2") 28 | ] 29 | ] 30 | where 31 | testDocS s = testDocS' (Text.unlines s) 32 | testDocS' s expected = expectParse parseList s (Right expected) 33 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Drawer/Logbook.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Data.OrgMode.Parse.Attoparsec.Drawer.Logbook 4 | -- Copyright : © 2017 Parnell Springmeyer 5 | -- License : All Rights Reserved 6 | -- Maintainer : Parnell Springmeyer 7 | -- Stability : stable 8 | -- 9 | -- Parsing combinators for org-mode section logbook drawers. 10 | ---------------------------------------------------------------------------- 11 | 12 | {-# LANGUAGE OverloadedStrings #-} 13 | 14 | module Data.OrgMode.Parse.Attoparsec.Drawer.Logbook 15 | ( 16 | parseLogbook 17 | ) 18 | where 19 | 20 | import Control.Applicative ((*>)) 21 | import Data.Attoparsec.Text 22 | import Data.Attoparsec.Types as Attoparsec 23 | import Data.Text (Text) 24 | 25 | import Data.OrgMode.Parse.Attoparsec.Drawer.Generic as Drawer.Generic 26 | import Data.OrgMode.Parse.Attoparsec.Time (parseClock) 27 | import Data.OrgMode.Types 28 | 29 | -- | Parse a @LOGBOOK@ drawer. 30 | -- 31 | -- > :LOGBOOK: 32 | -- > CLOCK: [2015-10-05 Mon 17:13]--[2015-10-05 Mon 17:14] => 0:01 33 | -- > :END: 34 | parseLogbook :: Attoparsec.Parser Text Logbook 35 | parseLogbook = Logbook <$> (drawerBegin *> manyTill parseClock Drawer.Generic.drawerEnd) 36 | where 37 | drawerBegin = Drawer.Generic.parseDrawerDelim "LOGBOOK" 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.3.0 2 | - Added support for parsing the following types of markup (thank you to 3 | @zhujinxuan for the significant contribution!): 4 | - `*bold*` 5 | - `/italic/` 6 | - `_underlined_` 7 | - `=verbatim=` 8 | - `~code~` 9 | - `+strikethrough` 10 | - LaTex markup 11 | 12 | 0.2.2 13 | - Support building with GHC 8.4.3 (thank you @zhujinxuan!) 14 | 15 | 0.2.1 16 | - Fix property drawer parsing of :END: delimiter, fixes #35 17 | 18 | 0.2.0 19 | - Fix timestamp parsing in headline and body, fixes #13 20 | - Generalize drawer parser for logbook and generic drawers, fixes #14 21 | - Reorganize the types, fixes #15 22 | 23 | 0.1.1.0 24 | - The weekday parser now correctly parses weekday appellations of other 25 | languages (thank you nushio3!) using a combinator-style version of the regex 26 | found in org-mode. 27 | 28 | 0.1.0.4 29 | - Comment improvement wibbles. 30 | - Adding the =Attoparsec= combinator modules to the export module list in the 31 | cabal package definition. 32 | 33 | 0.1.0.3 34 | - Sub-headings are now parsed and tracked by its parent. 35 | - Much more robust timestamp / clock / schedule parsing. 36 | - A good mount of code cleanup and comment improvement. 37 | 38 | 0.0.2.1 39 | - [X] Fixing the import for the =Internal= module (instead of re-exporting it in 40 | each parser module). 41 | 42 | 0.0.2.0 43 | - [X] Added parsers for the scheduled / deadline timestamps. 44 | 45 | 0.0.1.1 46 | - [X] Parsing of property drawer. 47 | - [X] Tests for both drawer and heading parsers. 48 | 49 | 0.0.0.2 50 | - [X] Parsers for orgmode list headlines. 51 | -------------------------------------------------------------------------------- /test/Headline.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE RankNTypes #-} 3 | 4 | module Headline where 5 | 6 | import Data.OrgMode.Parse 7 | import Test.Tasty 8 | import Test.Tasty.HUnit 9 | 10 | import Util 11 | 12 | parserHeadlineTests :: TestTree 13 | parserHeadlineTests = testGroup "Attoparsec Headline" 14 | [ testCase "Parse Headline Bare" $ testHeadline "* This is a title\n" 15 | , testCase "Parse Headline Bare with end colon" $ testHeadline "* This heading ends in a colon:" 16 | , testCase "Parse Headline Bare w/ Depths" $ testHeadline "*** This is a title\n" 17 | , testCase "Parse Headline w/ Priority" $ testHeadline "* [#A] An important heading\n" 18 | , testCase "Parse Headline w/ Priority & State" $ testHeadline "* TODO [#A] An important heading with a state indicator\n" 19 | , testCase "Parse Headline w/ State" $ testHeadline "* CANCELED An important heading with just state\n" 20 | , testCase "Parse Headline w/ Keywords" $ testHeadline "* An important heading :WITH:KEYWORDS:\n" 21 | , testCase "Parse Headline Full" $ testHeadline "* DONE [#B] A heading : with [[http://somelink.com][a link]] :WITH:KEYWORDS:\n" 22 | , testCase "Parse Headline All But Title" $ testHeadline "* DONE [#A] :WITH:KEYWORDS:\n" 23 | , testCase "Parse Headline w/ Timestamp" $ testHeadline "* TODO [#A] <2017-08-24 22:00> Pickup groceris on\n" 24 | ] 25 | where 26 | testHeadline = testParser (headlineBelowDepth ["TODO","CANCELED","DONE"] 0) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Parnell Springmeyer 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | - Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | - Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | - Neither the name of the Parnell Springmeyer nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /nix/fetchNixpkgs.nix: -------------------------------------------------------------------------------- 1 | { rev # The Git revision of nixpkgs to fetch 2 | , sha256 # The SHA256 of the downloaded data 3 | , outputSha256 ? null # The SHA256 output hash 4 | , system ? builtins.currentSystem # This is overridable if necessary 5 | }: 6 | 7 | if (0 <= builtins.compareVersions builtins.nixVersion "1.12") 8 | 9 | # In Nix 1.12, we can just give a `sha256` to `builtins.fetchTarball`. 10 | then ( 11 | builtins.fetchTarball { 12 | url = "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz"; 13 | sha256 = outputSha256; 14 | }) 15 | 16 | # This hack should at least work for Nix 1.11 17 | else ( 18 | (rec { 19 | tarball = import { 20 | url = "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz"; 21 | inherit sha256; 22 | }; 23 | 24 | builtin-paths = import ; 25 | 26 | script = builtins.toFile "nixpkgs-unpacker" '' 27 | "$coreutils/mkdir" "$out" 28 | cd "$out" 29 | "$gzip" --decompress < "$tarball" | "$tar" -x --strip-components=1 30 | ''; 31 | 32 | nixpkgs = builtins.derivation ({ 33 | name = "nixpkgs-${builtins.substring 0 6 rev}"; 34 | 35 | builder = builtins.storePath builtin-paths.shell; 36 | 37 | args = [ script ]; 38 | 39 | inherit tarball system; 40 | 41 | tar = builtins.storePath builtin-paths.tar; 42 | gzip = builtins.storePath builtin-paths.gzip; 43 | coreutils = builtins.storePath builtin-paths.coreutils; 44 | } // (if null == outputSha256 then { } else { 45 | outputHashMode = "recursive"; 46 | outputHashAlgo = "sha256"; 47 | outputHash = outputSha256; 48 | })); 49 | }).nixpkgs) 50 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Content.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Data.OrgMode.Parse.Attoparsec.Content 4 | -- Copyright : © 2014 Parnell Springmeyer 5 | -- License : All Rights Reserved 6 | -- Maintainer : Parnell Springmeyer 7 | -- Stability : stable 8 | -- 9 | -- Parsing combinators for org-mode markup and paragraphs. 10 | ---------------------------------------------------------------------------- 11 | 12 | module Data.OrgMode.Parse.Attoparsec.Content 13 | ( 14 | parseContents 15 | ) 16 | where 17 | 18 | import Data.Attoparsec.Text (Parser, 19 | eitherP, 20 | many') 21 | import Data.Semigroup ((<>)) 22 | 23 | import Data.OrgMode.Parse.Attoparsec.Content.List (parseList) 24 | import Data.OrgMode.Parse.Attoparsec.Content.Paragraph (parseParagraph) 25 | import Data.OrgMode.Parse.Attoparsec.Drawer (parseDrawer) 26 | import Data.OrgMode.Parse.Attoparsec.Util (parseLinesTill, 27 | takeContentBreak) 28 | import Data.OrgMode.Types (Content (..)) 29 | 30 | 31 | -- | Parse the content until reaching a drawer, a list, or a content end. And include the parsed drawer. 32 | parseContents :: Parser [Content] 33 | parseContents = concat <$> many' p 34 | where 35 | p :: Parser [Content] 36 | p = do 37 | blocks <- parseLinesTill parseParagraph (eitherP takeContentBreak (parseDrawer <> parseList)) 38 | return $ filter (/= Paragraph []) blocks 39 | -------------------------------------------------------------------------------- /test/Content/Paragraph.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Content.Paragraph ( 4 | parserParagraphs 5 | ) 6 | where 7 | import Test.Tasty 8 | import Test.Tasty.HUnit 9 | import Data.OrgMode.Types 10 | import Data.Text (Text, pack) 11 | import Data.OrgMode.Parse.Attoparsec.Content.Paragraph (parseParagraph) 12 | import Util 13 | import Util.Builder 14 | 15 | testDocS :: (ContentBuilder t) => Text -> t -> Assertion 16 | testDocS s expected = expectParse parseParagraph s (Right (toP expected)) 17 | 18 | parserParagraphs :: TestTree 19 | parserParagraphs = testGroup "Attoparsec orgmode Paragraph" 20 | [ testCase "Parses a Single Markup" $ 21 | testDocS "* text *" (mark Bold (pack "text")), 22 | testCase "Parses a Plain Text (with line break)" $ 23 | testDocS " text \n" (pack " text"), 24 | testCase "Parses a Plain Text (without line break)" $ 25 | testDocS " text " (pack " text"), 26 | testCase "Parses a broken markup with token at start" $ 27 | testDocS "_ text" (pack "_ text"), 28 | testCase "Parses a broken markup Paragraph with token at end" $ 29 | testDocS " text *" (pack " text *"), 30 | testCase "Parses a broken markup Paragraph with token in middle" $ 31 | testDocS " te*xt " (pack " te*xt"), 32 | testCase "Parses Nested Markup" $ 33 | testDocS "_* text *_" $ mark (UnderLine . (:[]) . Bold) (pack "text"), 34 | testCase "Paragraph Parser shall not try to parse markup across lines" $ 35 | testDocS "_* l1p1 \nl2p2 *_" $ mark (UnderLine . (:[]) . Bold) (pack "l1p1 l2p2"), 36 | testCase "Paragraph Parser shall ignore the space before line end (in plain)" $ 37 | testDocS " l1p1 \nl2p2 " (pack " l1p1 l2p2"), 38 | testCase "Paragraph Parser shall ignore the markup inside verbatim" $ 39 | testDocS "= *l1p1 l2p2* =" $ Paragraph [Verbatim " *l1p1 l2p2* "] 40 | ] 41 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Document.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Data.OrgMode.Parse.Attoparsec.Document 4 | -- Copyright : © 2014 Parnell Springmeyer 5 | -- License : All Rights Reserved 6 | -- Maintainer : Parnell Springmeyer 7 | -- Stability : stable 8 | -- 9 | -- Top-level attoparsec parser for org-mode documents. 10 | ---------------------------------------------------------------------------- 11 | 12 | module Data.OrgMode.Parse.Attoparsec.Document 13 | ( parseDocument 14 | , parseDocumentWithKeywords 15 | ) 16 | where 17 | 18 | import Control.Applicative ((<$>), (<*>)) 19 | import Data.Attoparsec.Text 20 | import Data.Attoparsec.Types as Attoparsec 21 | import Data.Text (Text) 22 | import qualified Data.Text as Text 23 | 24 | import qualified Data.OrgMode.Parse.Attoparsec.Constants as Constants 25 | import Data.OrgMode.Parse.Attoparsec.Headline 26 | import qualified Data.OrgMode.Parse.Attoparsec.Util as Util 27 | import Data.OrgMode.Types 28 | 29 | ------------------------------------------------------------------------------ 30 | -- | Parse a document. 31 | -- 32 | -- This function uses the following default set of state keywords: 33 | -- - @TODO@ 34 | -- - @DONE@ 35 | -- - @CANCELLED@ 36 | -- 37 | -- See 'parseDocumentWithKeywords' for a version of the function that 38 | -- accepts a list of custom state keywords. 39 | parseDocument :: Attoparsec.Parser Text Document 40 | parseDocument = parseDocumentWithKeywords Constants.keywords 41 | 42 | -- | Parse a document with a custom list of state keywords. 43 | parseDocumentWithKeywords :: [Text] -> Attoparsec.Parser Text Document 44 | parseDocumentWithKeywords otherKeywords = 45 | Document 46 | <$> (Text.unlines <$> many' Util.nonHeadline) 47 | <*> many' (headlineBelowDepth otherKeywords 0) 48 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Section.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Data.OrgMode.Parse.Attoparsec.Section 4 | -- Copyright : © 2015 Parnell Springmeyer 5 | -- License : All Rights Reserved 6 | -- Maintainer : Parnell Springmeyer 7 | -- Stability : stable 8 | -- 9 | -- Parsing combinators for org-mode headline sections. 10 | ---------------------------------------------------------------------------- 11 | 12 | 13 | 14 | module Data.OrgMode.Parse.Attoparsec.Section where 15 | 16 | import Control.Applicative (optional) 17 | import Data.Attoparsec.Text (many', option, 18 | skipSpace) 19 | import Data.Monoid () 20 | 21 | import Data.OrgMode.Parse.Attoparsec.Content (parseContents) 22 | import Data.OrgMode.Parse.Attoparsec.Drawer 23 | import Data.OrgMode.Parse.Attoparsec.Time (parseClock, 24 | parsePlannings, 25 | parseTimestamp) 26 | import Data.OrgMode.Parse.Attoparsec.Util (skipEmptyLines) 27 | import Data.OrgMode.Types 28 | 29 | import qualified Data.Attoparsec.Text as Attoparsec.Text 30 | 31 | -- | Parse a heading section 32 | -- 33 | -- Headline sections contain optionally a property drawer, 34 | -- a list of clock entries, code blocks (not yet implemented), 35 | -- plain lists (not yet implemented), and unstructured text. 36 | parseSection :: Attoparsec.Text.Parser Section 37 | parseSection = skipEmptyLines *> parseSection' <* skipEmptyLines 38 | where 39 | parseSection' = Section 40 | <$> optional (skipSpace *> parseTimestamp <* skipSpace) 41 | <*> parsePlannings 42 | <*> many' parseClock 43 | <*> option mempty parseProperties 44 | <*> option mempty parseLogbook 45 | <*> parseContents 46 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Util.hs: -------------------------------------------------------------------------------- 1 | {-| 2 | Module : Data.OrgMode.Parse.Attoparsec.Util 3 | Copyright : © 2017 Parnell Springmeyer 4 | License : All Rights Reserved 5 | Maintainer : Parnell Springmeyer 6 | Stability : stable 7 | 8 | Attoparsec utilities. 9 | -} 10 | 11 | 12 | module Data.OrgMode.Parse.Attoparsec.Util 13 | ( skipOnlySpace, 14 | nonHeadline, 15 | module Data.OrgMode.Parse.Attoparsec.Util.ParseLinesTill 16 | ) 17 | 18 | where 19 | 20 | import Data.Attoparsec.Text (Parser, 21 | endOfLine, 22 | isEndOfLine, 23 | isHorizontalSpace, 24 | notChar, 25 | takeTill) 26 | import Data.Functor (($>)) 27 | import Data.Semigroup ((<>)) 28 | import Data.Text (Text, cons) 29 | 30 | import Data.OrgMode.Parse.Attoparsec.Util.ParseLinesTill 31 | 32 | import qualified Data.Attoparsec.Text as Attoparsec.Text 33 | import qualified Data.Text as Text 34 | 35 | -- | Skip whitespace characters, only! 36 | -- 37 | -- @Data.Attoparsec.Text.skipSpace@ uses the @isSpace@ predicate from 38 | -- @Data.Char@ which also includes control characters such as a return 39 | -- and newline which we need to *not* consume in some cases during 40 | -- parsing. 41 | skipOnlySpace :: Parser () 42 | skipOnlySpace = Attoparsec.Text.skipWhile isHorizontalSpace 43 | 44 | -- | Parse a non-heading line of a section. 45 | nonHeadline :: Parser Text 46 | nonHeadline = nonHeadline0 <> nonHeadline1 47 | where 48 | nonHeadline0 = endOfLine $> Text.empty 49 | nonHeadline1 = cons <$> notChar '*' <*> (takeTill isEndOfLine <* endOfLine) 50 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Drawer/Generic.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Data.OrgMode.Parse.Attoparsec.Drawer.Generic 4 | -- Copyright : © 2017 Parnell Springmeyer 5 | -- License : All Rights Reserved 6 | -- Maintainer : Parnell Springmeyer 7 | -- Stability : stable 8 | -- 9 | -- Parsing combinators for org-mode section generic drawers. 10 | ---------------------------------------------------------------------------- 11 | 12 | 13 | {-# LANGUAGE OverloadedStrings #-} 14 | 15 | module Data.OrgMode.Parse.Attoparsec.Drawer.Generic 16 | ( parseDrawer 17 | , parseDrawerDelim 18 | , drawerEnd 19 | ) 20 | where 21 | 22 | import Control.Applicative ((*>), (<*)) 23 | import Data.Attoparsec.Text (Parser, asciiCI, char, 24 | manyTill, skipSpace, 25 | takeWhile1, ()) 26 | import Data.Text (Text) 27 | 28 | import qualified Data.OrgMode.Parse.Attoparsec.Util as Util 29 | import Data.OrgMode.Types 30 | 31 | import qualified Data.Text as Text 32 | 33 | -- | Parse a user-defined drawer. 34 | -- 35 | -- > :MYTEXT: 36 | -- > whatever I want, can go in here except for headlines and drawers 37 | -- > :END: 38 | parseDrawer :: Parser Drawer 39 | parseDrawer = 40 | Drawer <$> 41 | parseDrawerName <*> 42 | (Text.unlines <$> manyTill Util.nonHeadline drawerEnd) 43 | 44 | -- | Parse a user-defined drawer's name, e.g: 45 | -- 46 | -- > :DRAWERNAME: 47 | -- > my text in a drawer 48 | -- > :END: 49 | parseDrawerName :: Parser Text 50 | parseDrawerName = 51 | skipSpace *> char ':' *> 52 | takeWhile1 (/= ':') <* 53 | char ':' <* skipSpace 54 | 55 | -- | Parse drawer delimiters, e.g the beginning and end of a property 56 | -- drawer: 57 | -- 58 | -- > :PROPERTIES: 59 | -- > :END: 60 | parseDrawerDelim :: Text -> Parser Text 61 | parseDrawerDelim v = 62 | skipSpace *> char ':' *> 63 | asciiCI v <* 64 | char ':' <* Util.skipOnlySpace 65 | 66 | -- | Parse the @:END:@ of a drawer. 67 | drawerEnd :: Parser Text 68 | drawerEnd = parseDrawerDelim "END" "Expected Drawer End :END:" 69 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Drawer/Property.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Data.OrgMode.Parse.Attoparsec.Drawer.Property 4 | -- Copyright : © 2014 Parnell Springmeyer 5 | -- License : All Rights Reserved 6 | -- Maintainer : Parnell Springmeyer 7 | -- Stability : stable 8 | -- 9 | -- Parsing combinators for org-mode section property drawers. 10 | ---------------------------------------------------------------------------- 11 | 12 | {-# LANGUAGE OverloadedStrings #-} 13 | 14 | module Data.OrgMode.Parse.Attoparsec.Drawer.Property 15 | ( parseProperties 16 | , property 17 | , PropertyKey 18 | , PropertyVal 19 | ) 20 | where 21 | 22 | import Control.Applicative ((*>), (<*)) 23 | import Data.Attoparsec.Text as T 24 | import Data.Attoparsec.Types as Attoparsec 25 | import Data.HashMap.Strict.InsOrd (fromList) 26 | import Data.Text (Text) 27 | 28 | import Data.OrgMode.Parse.Attoparsec.Drawer.Generic as Drawer.Generic 29 | import Data.OrgMode.Types 30 | 31 | import qualified Data.Text as Text 32 | 33 | type PropertyKey = Text 34 | type PropertyVal = Text 35 | 36 | -- | Parse a @PROPERTY@ drawer. 37 | -- 38 | -- > :PROPERTIES: 39 | -- > :DATE: [2014-12-14 11:00] 40 | -- > :NOTE: Something really crazy happened today! 41 | -- > :END: 42 | parseProperties :: Attoparsec.Parser Text Properties 43 | parseProperties = Properties . fromList <$> (drawerBegin *> manyTill property Drawer.Generic.drawerEnd) 44 | where 45 | drawerBegin = Drawer.Generic.parseDrawerDelim "PROPERTIES" 46 | 47 | -- | Parse a property of a drawer. 48 | -- 49 | -- Properties *must* be a `:KEY: value` pair, the key can be of any 50 | -- case and contain any characters except for newlines and colons 51 | -- (since they delimit the start and end of the key). 52 | property :: Attoparsec.Parser Text (PropertyKey, PropertyVal) 53 | property = (,) <$> parseKey <*> parseVal 54 | where 55 | parseKey = skipSpace *> skip (== ':') *> takeWhile1 (/= ':') <* skip (== ':') 56 | parseVal = Text.strip <$> (skipSpace *> takeValue) 57 | takeValue = takeWhile1 (not . isEndOfLine) 58 | -------------------------------------------------------------------------------- /test/Content/Contents.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE TypeApplications #-} 3 | 4 | module Content.Contents 5 | ( parserContents 6 | ) 7 | where 8 | 9 | import Data.OrgMode.Parse.Attoparsec.Content (parseContents) 10 | import Data.OrgMode.Types 11 | import Data.Text (Text) 12 | import Test.Tasty 13 | import Test.Tasty.HUnit 14 | import Util 15 | import Util.Builder 16 | 17 | import qualified Data.Text as Text 18 | 19 | testDocS :: [Text] -> [Content] -> Assertion 20 | testDocS s expected = expectParse parseContents (Text.unlines s) (Right expected) 21 | 22 | parserContents :: TestTree 23 | parserContents = testGroup "Attoparsec orgmode Section Contents" 24 | [ testCase "Parses a Single Paragraph" $ 25 | testDocS 26 | ["*text *"] 27 | [mark Bold ("text" :: Text)] 28 | 29 | , testCase "Parses a HyperLink" $ 30 | testDocS 31 | [ "[[https://orgmode.org/manual/Link-format.html][The Org Manual: Link format]]" ] 32 | [ Paragraph 33 | [ HyperLink "https://orgmode.org/manual/Link-format.html" (Just "The Org Manual: Link format") ] 34 | ] 35 | 36 | , testCase "Parses an italicised HyperLink" $ 37 | testDocS 38 | [ "/[[Headline 2][The Org Manual: Link format]]/" ] 39 | [ Paragraph [ 40 | Italic [ 41 | HyperLink "Headline 2" (Just "The Org Manual: Link format") 42 | ] 43 | ] 44 | ] 45 | 46 | , testCase "Parses an italicised HyperLink in an unordered list" $ 47 | testDocS 48 | [ " - /[[Headline 2][The Org Manual: Link format]]/" ] 49 | [ UnorderedList [ 50 | Item [ 51 | Paragraph [ 52 | Italic [ 53 | HyperLink "Headline 2" (Just "The Org Manual: Link format") 54 | ] 55 | ] 56 | ] 57 | ] 58 | ] 59 | 60 | , testCase "Parses Paragraph and List" $ 61 | testDocS 62 | ["*text *", " * item1 "] 63 | [mark Bold ("text" :: Text), UnorderedList [toI @Text "item1"]] 64 | 65 | , testCase "Parses Paragraph and List with blank line" $ 66 | testDocS 67 | ["*text *", " ", " * item1"] 68 | [mark Bold ("text" :: Text), UnorderedList [toI @Text "item1"]] 69 | 70 | , testCase "Parses List and Paragraph" $ 71 | testDocS 72 | [ " * item1", "*text *"] 73 | [UnorderedList [toI @Text "item1"], mark Bold ("text" :: Text)] 74 | , testCase "Parses List and Paragraph with blank line" $ 75 | testDocS 76 | [ " * item1", " ", "*text *"] 77 | [UnorderedList [toI @Text "item1"], mark Bold ("text" :: Text)] 78 | ] 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome! 2 | ![Hackage Version](https://img.shields.io/hackage/v/orgmode-parse.svg?style=flat) 3 | ![Travis CI Status](https://travis-ci.org/ixmatus/orgmode-parse.svg?branch=master) 4 | 5 | `orgmode-parse` provides a top-level parser and collection of attoparsec parser 6 | combinators for org-mode structured text. 7 | 8 | - [What's Finished](#whats-finished) 9 | - [Building](#building) 10 | 11 | You can find the package on [Hackage](https://hackage.haskell.org/package/orgmode-parse). 12 | 13 | ## What's Finished 14 | We have built attoparsec parsers for parsing org-mode document structures and 15 | meta-data. Here is a list of [all the syntax features](https://orgmode.org/worg/dev/org-syntax.html) that have a complete 16 | parsing implementation and not: 17 | 18 | - [X] Headlines and Sections 19 | - [ ] Affiliated Keywords 20 | - [-] Greater Elements 21 | - [ ] Greater Blocks 22 | - [X] Drawers 23 | - [ ] Dynamic Blocks 24 | - [ ] Footnote Definitions 25 | - [ ] Inlinetasks 26 | - [ ] Plain Lists and Items 27 | - [X] Unordered lists 28 | - [X] Numbered lists 29 | - [ ] Checkbox modified lists 30 | - [X] Property Drawers 31 | - [ ] Tables 32 | - [ ] Elements 33 | - [ ] Babel Cell 34 | - [ ] Blocks 35 | - [X] Clock, Diary Sexp and Planning 36 | - [X] Scheduled and deadline timestamps (timestamp, range, duration, periodicity) 37 | - [X] Active and inactive timestamps 38 | - [X] Clock timestamps 39 | - [ ] Comments 40 | - [ ] Fixed Width Areas 41 | - [ ] Horizontal Rules 42 | - [X] Keywords 43 | - [ ] LaTeX Environments 44 | - [X] Node Properties 45 | - [X] Paragraphs 46 | - [ ] Table Rows 47 | - [ ] Objects 48 | - [-] Entities and LaTeX Fragments 49 | - [ ] Export Snippets 50 | - [ ] Footnote References 51 | - [ ] Inline Babel Calls and Source Blocks 52 | - [ ] Line Breaks (\\) 53 | - [ ] Links 54 | - [ ] Macros 55 | - [ ] Targets and Radio Targets 56 | - [ ] Statistics Cookies 57 | - [ ] Table Cells 58 | - [-] Timestamps 59 | - [ ] Text Markup 60 | - [X] Bold 61 | - [X] Italic 62 | - [X] Strikethrough 63 | - [X] Underline 64 | - [ ] Superscript 65 | - [ ] Subscript 66 | - [X] Code / monospaced 67 | - [ ] Position Annotated AST 68 | 69 | ## Building 70 | There are a few ways to build this library if you're developing a patch: 71 | 72 | - `stack build && stack test`, and 73 | - `nix-build --no-out-link --attr orgmode-parse release.nix` 74 | 75 | You can also use the `nix-shell` provided cabal environment for incremental 76 | development: 77 | 78 | ```shell 79 | $ nix-shell 80 | $ cabal build 81 | ``` 82 | 83 | ## Projects that use this package: 84 | https://github.com/volhovm/orgstat 85 | 86 | # License 87 | [BSD3 Open Source Software License](https://github.com/digitalmentat/orgmode-parse/blob/master/LICENSE) 88 | -------------------------------------------------------------------------------- /test/Timestamps.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE RankNTypes #-} 3 | {-# LANGUAGE RecordWildCards #-} 4 | 5 | module Timestamps where 6 | 7 | import Control.Applicative ((<*)) 8 | import Data.Attoparsec.Text (endOfLine) 9 | import Data.Semigroup ((<>)) 10 | import Data.Maybe (isNothing) 11 | import Data.OrgMode.Parse 12 | import qualified Data.Text as T 13 | import Test.Tasty 14 | import Test.Tasty.HUnit 15 | import Weekdays (weekdays) 16 | 17 | import Data.OrgMode.Types 18 | import Util 19 | 20 | parserPlanningTests :: TestTree 21 | parserPlanningTests = testGroup "Attoparsec Planning" 22 | [ testCase "Parse Planning Schedule" $ testPlanning "SCHEDULED: <2004-02-29 Sun>" 23 | , testCase "Parse Planning Deadline" $ testPlanning "DEADLINE: <2004-02-29 Sun>" 24 | , testCase "Parse Planning Full" $ testPlanning "SCHEDULED: <2004-02-29 Sun +1w>" 25 | , testCase "Parse Sample Schedule" $ testPlanningS sExampleStrA sExampleResA 26 | ] 27 | where 28 | testPlanning = testParser parsePlannings 29 | testPlanningS t r = expectParse parsePlannings t (Right r) 30 | 31 | (sExampleStrA, sExampleResA) = ( "SCHEDULED: <2004-02-29 Sun 10:20 +1w -2d>", [ Planning{..} ] ) 32 | where 33 | keyword = SCHEDULED 34 | timestamp = 35 | Timestamp 36 | (DateTime 37 | (YearMonthDay 2004 2 29) 38 | (Just "Sun") 39 | (Just (10,20)) 40 | (Just (Repeater RepeatCumulate 1 UnitWeek)) 41 | (Just (Delay DelayAll 2 UnitDay)) 42 | ) 43 | Active 44 | Nothing 45 | 46 | parserTimestampTests :: TestTree 47 | parserTimestampTests = testGroup "Attoparsec Timestamp" 48 | [ testCase "Parse Timestamp Appointment" $ testTimestamp "<2004-02-29 Sun>\n" 49 | , testCase "Parse Timestamp Recurring" $ testTimestamp "<2004-02-29 Sun +1w>\n" 50 | ] 51 | where 52 | testTimestamp = testParser (parseTimestamp <* endOfLine) 53 | 54 | 55 | parserWeekdayTests :: TestTree 56 | parserWeekdayTests = testGroup "Attoparsec Weekday" 57 | [testCase ("Parse Weekday in " ++ loc) $ mkTest w 58 | | (loc,ws) <- weekdays, w <- ws, isOrgParsable w] 59 | where 60 | dayChars = "]+0123456789>\r\n -" :: String 61 | isOrgParsable w = isNothing (T.find (`elem` dayChars) w) 62 | mkTest w = expectParse parseTimestamp str (Right res) 63 | where 64 | str = "<2004-02-29 " <> w <> " 10:20>" 65 | res = Timestamp (DateTime 66 | (YearMonthDay 2004 2 29) 67 | (Just w) 68 | (Just (10,20)) 69 | Nothing 70 | Nothing) Active Nothing 71 | -------------------------------------------------------------------------------- /haddock2hackage: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Options / Usage 4 | # put this script in the same directory as your *.cabal file 5 | # it will use the first line of "cabal info ." to determine the package name 6 | 7 | # custom options for "cabal haddock" (cabal haddock --help, 8 | # http://www.haskell.org/haddock/doc/html/invoking.html) 9 | CUSTOM_OPTIONS=(--haddock-options='-q aliased') 10 | # hackage server to upload to (and to search uploaded versions for) 11 | HACKAGESERVER=hackage.haskell.org 12 | # whether to use cabal install (1) or copy docs directly from cabal haddock (0) 13 | # some user had troubles installing their package (or dependencies) 14 | CABAL_INSTALL=0 15 | # put your credentials into ~/.netrc: (see man netrc) 16 | # machine $HACKAGESERVER 17 | # login $USERNAME 18 | # password $PASSWORD 19 | 20 | # nothing to configure below this line 21 | 22 | # How it works 23 | # 24 | # It tries to find your package on the given hackage server, and 25 | # uploads the generated -doc.tar.gz. 26 | # It first tries the released version, then the candidate. 27 | # 28 | # To generate the docs it uses "cabal install" to install into a temporary directory, 29 | # with a temporary ghc package db in it. 30 | 31 | set -e 32 | 33 | status_code() { 34 | local code=$(curl "http://${HACKAGESERVER}$1" --silent -o /dev/null --write-out %{http_code}) 35 | echo "http://${HACKAGESERVER}$1 $code" >&2 36 | echo $code 37 | } 38 | 39 | self=$(readlink -f "$0") 40 | base=$(dirname "${self}") 41 | cd "${base}" 42 | tmpdir=$(mktemp --tmpdir -d doc-package-XXXXXXX) 43 | trap 'rm -rf "${tmpdir}"' EXIT 44 | 45 | name=$(cabal info . 2>/dev/null | awk '{print $2;exit}') 46 | plain_name="${name%-*}" # strip version number (must not contain a '-', the name itself can) 47 | 48 | if [ "200" = "$(status_code /package/${name})" ]; then 49 | echo "Found released version ${name}" 50 | targeturl="/package/${name}/docs" 51 | elif [ "200" = "$(status_code /package/${name}/candidate)" ]; then 52 | echo "Found candidate version ${name}" 53 | targeturl="/package/${name}/candidate/docs" 54 | else 55 | echo "Found no uploaded version" 56 | targeturl="" 57 | fi 58 | 59 | 60 | prefix="${tmpdir}" 61 | docdir="${prefix}/share/doc/${name}" 62 | if [ "${CABAL_INSTALL}" = 1 ]; then 63 | # after cabal install: 64 | htmldir="${docdir}/html" 65 | else 66 | # without cabal install: 67 | htmldir="${tmpdir}/dist/doc/html/${plain_name}" 68 | fi 69 | 70 | packagedb="${tmpdir}/package.conf.d" 71 | mkdir -p "${packagedb}" 72 | pkgdocdir="${tmpdir}/${name}-docs" 73 | pkgdocarchive="${tmpdir}/${name}-doc.tar.gz" 74 | 75 | cabal configure \ 76 | --builddir="${tmpdir}/dist" \ 77 | --disable-optimization --ghc-option -O0 \ 78 | --docdir="${docdir}" \ 79 | --prefix="${prefix}" 80 | 81 | # need separate haddock step, as install doesn't forward --builddir to haddock with 82 | # cabal install --enable-documentation 83 | # otherwise configure+haddock could be merged into install 84 | # (prefix cabal haddock options with --haddock- for cabal install) 85 | cabal haddock \ 86 | --builddir="${tmpdir}/dist" \ 87 | --html-location='/package/$pkg-$version/docs' \ 88 | --haddock-option='--built-in-themes' \ 89 | --hoogle --html \ 90 | "${CUSTOM_OPTIONS[@]}" \ 91 | --contents-location='/package/$pkg-$version' \ 92 | --hyperlink-source 93 | 94 | if [ "${CABAL_INSTALL}" = 1 ]; then 95 | cabal install \ 96 | --builddir="${tmpdir}/dist" \ 97 | --docdir="${docdir}" \ 98 | --prefix="${prefix}" \ 99 | --ghc-pkg-option --no-user-package-conf \ 100 | --ghc-pkg-option --package-db="${packagedb}" 101 | fi 102 | 103 | cp -ar "${htmldir}" "${pkgdocdir}" 104 | (cd "$(dirname ${pkgdocdir})"; tar --format=ustar -caf "${pkgdocarchive}" "$(basename ${pkgdocdir})") 105 | mkdir -p dist/ 106 | echo "Copying $(basename ${pkgdocdir}) to dist/" 107 | cp -ar "${pkgdocarchive}" dist/ 108 | 109 | if [ "${targeturl}" != "" ]; then 110 | echo -n "Upload to http://${HACKAGESERVER}${targeturl} (y/N)? " 111 | read ack 112 | if [ "${ack}" = "y" -o "${ack}" = "Y" ]; then 113 | echo "Uploading..." 114 | curl \ 115 | -X PUT \ 116 | -H "Content-Type: application/x-tar" \ 117 | -H "Content-Encoding: gzip" \ 118 | --data-binary @"${pkgdocarchive}" \ 119 | --digest --netrc \ 120 | "http://${HACKAGESERVER}${targeturl}" 121 | else 122 | echo "Not uploading." 123 | fi 124 | fi 125 | 126 | echo Done. 127 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Content/List.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Data.OrgMode.Parse.Attoparsec.Content.List 4 | -- Copyright : © 2014 Parnell Springmeyer 5 | -- License : All Rights Reserved 6 | -- Maintainer : Parnell Springmeyer 7 | -- Stability : stable 8 | -- 9 | -- Parsing combinators for org-mode markups and paragraphs. 10 | ---------------------------------------------------------------------------- 11 | 12 | {-# LANGUAGE ScopedTypeVariables #-} 13 | 14 | 15 | module Data.OrgMode.Parse.Attoparsec.Content.List 16 | ( 17 | parseList 18 | ) 19 | where 20 | 21 | import Control.Monad (guard) 22 | import Data.Attoparsec.Text (Parser, char, 23 | digit, 24 | isHorizontalSpace, 25 | many1') 26 | import Data.Functor (($>)) 27 | import Data.Maybe (isNothing) 28 | 29 | import Data.OrgMode.Parse.Attoparsec.Content.Paragraph (parseParagraph) 30 | import Data.OrgMode.Parse.Attoparsec.Util (parseLinesContextuallyTill, 31 | parseLinesTill, 32 | takeContentBreak) 33 | import Data.OrgMode.Types (Content (..), 34 | Item (..)) 35 | 36 | import qualified Data.Attoparsec.Text as Attoparsec.Text 37 | import qualified Data.Text as Text 38 | 39 | type TokenParser = Parser ([Item] -> Content) 40 | 41 | -- | Parser will success when the parser position is in a new item or break out from the list 42 | -- 43 | -- The Bool determines that whether it is in the first line and therefore ignore the preceding space checks 44 | breakout :: forall b. Int -> Bool -> (Bool, Parser (Either () b)) 45 | breakout n isInFirstLine = (False, result isInFirstLine) 46 | where 47 | -- fail will take the following text in the same line into the 48 | -- Item content see details in parseLinesTill of 49 | -- Util/ParseLinesTill.hs 50 | result x= guard (not x) *> do 51 | z <- Attoparsec.Text.takeWhile isHorizontalSpace 52 | -- If not enough space in the new line, then it shall be another 53 | -- item or a new list 54 | guard (Text.compareLength z n == LT) $> Left () 55 | 56 | takeHorizontalSpaces :: Int -> Parser () 57 | takeHorizontalSpaces n = Attoparsec.Text.take n >>= assertSpaces 58 | where 59 | assertSpaces t = guard (isNothing (Text.find (not . isHorizontalSpace) t)) 60 | 61 | parseItemTokens :: [TokenParser] 62 | parseItemTokens = ordered ++ unordered 63 | where 64 | ordered = [many1' digit $> OrderedList] 65 | unordered = map parseToken ['*', '-'] 66 | parseToken x = char x $> UnorderedList 67 | 68 | data ItemTerm = ItemTerm 69 | { parseNext :: Parser Item 70 | , toListContent :: [Item] -> Content 71 | , item :: Item 72 | } 73 | 74 | -- | Create a Parser to Parse Item 75 | parseItemTermVia :: TokenParser -> Parser ItemTerm 76 | parseItemTermVia p = result 77 | where 78 | result :: Parser ItemTerm 79 | result = do 80 | n <- Text.length <$> Attoparsec.Text.takeWhile1 isHorizontalSpace 81 | ItemTerm (parseItem n) <$> p <*> (takeHorizontalSpaces 1*> parseItemCore n) 82 | 83 | parseItem :: Int -> Parser Item 84 | parseItem n = takeHorizontalSpaces n *> p *> takeHorizontalSpaces 1*> parseItemCore n 85 | 86 | parseItemCore :: Int -> Parser Item 87 | parseItemCore n = Item . concat <$> parseLinesContextuallyTill parseContents (breakout (n+1)) True 88 | 89 | parseContents :: Parser [Content] 90 | parseContents = concat <$> Attoparsec.Text.many' (parseLinesTill parseParagraph (Attoparsec.Text.eitherP takeContentBreak parseList)) 91 | 92 | parseItemTerm :: Parser ItemTerm 93 | parseItemTerm = Attoparsec.Text.choice (map parseItemTermVia parseItemTokens) 94 | 95 | parseList :: Parser Content 96 | parseList = do 97 | term <- parseItemTerm 98 | items <- Attoparsec.Text.many' (parseNext term) 99 | return $ toListContent term (item term:items) 100 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Util/ParseLinesTill.hs: -------------------------------------------------------------------------------- 1 | {-| 2 | Module : Data.OrgMode.Parse.Attoparsec.Util.ParseLinesTill 3 | Copyright : © 2017 Parnell Springmeyer 4 | License : All Rights Reserved 5 | Maintainer : Parnell Springmeyer 6 | Stability : stable 7 | 8 | Attoparsec utilities 9 | -} 10 | 11 | {-# LANGUAGE CPP #-} 12 | {-# LANGUAGE ScopedTypeVariables #-} 13 | 14 | module Data.OrgMode.Parse.Attoparsec.Util.ParseLinesTill 15 | ( takeALine 16 | , ParseLinesTill (..) 17 | , takeContentBreak 18 | , skipEmptyLines 19 | ) 20 | where 21 | 22 | 23 | import Control.Arrow ((&&&)) 24 | import Control.Monad (guard) 25 | #if __GLASGOW_HASKELL__ >= 810 26 | import Data.Bifoldable (bifoldMap) 27 | #endif 28 | import Data.Attoparsec.Text (Parser, anyChar, atEnd, char, endOfLine, 29 | isEndOfLine, isHorizontalSpace, many1, 30 | parseOnly, takeTill, ()) 31 | import Data.Foldable (Foldable (..)) 32 | import Data.Functor (($>)) 33 | import Data.Semigroup ((<>)) 34 | import Data.Text (Text, snoc) 35 | 36 | import qualified Control.Monad 37 | import qualified Data.Attoparsec.Text as Attoparsec.Text 38 | import qualified Data.Text as Text 39 | 40 | takeALine :: Parser Text 41 | takeALine = do 42 | content <- takeTill isEndOfLine 43 | Attoparsec.Text.option content (snoc content <$> anyChar) 44 | 45 | -- | Match only if there are more content to be consumed. 46 | -- 47 | -- Opposite of @Attoparsec.Text.endOfInput@ 48 | hasMoreInput :: Parser () 49 | hasMoreInput = do 50 | x <- atEnd 51 | Control.Monad.when x $ fail "reached the end of input" 52 | 53 | -- | Matches only if the incoming text line consists nothing or only spaces 54 | -- 55 | -- A empty line always ends a SectionContent 56 | takeEmptyLine :: Parser Text 57 | takeEmptyLine = Attoparsec.Text.takeWhile isHorizontalSpace <* endOfLine 58 | 59 | -- | Succeed if it is a headline 60 | headline :: Parser () 61 | headline = hasHeadlinePrefix *> atLeastOneSpace 62 | where 63 | hasHeadlinePrefix = many1 (char '*') 64 | 65 | atLeastOneSpace :: Parser () 66 | atLeastOneSpace = do 67 | z <- anyChar 68 | guard (isHorizontalSpace z) "A space must follow the last * of a headline" 69 | 70 | -- | Is the current line a @SectionContent@ break. A Line is a break 71 | takeContentBreak :: Parser () 72 | takeContentBreak = breakByEmptyLine <> headline 73 | where 74 | breakByEmptyLine = takeEmptyLine $> () 75 | 76 | -- | Transform a text content as block to work with current parser state 77 | feedParserText :: Parser s -> Text -> Parser s 78 | 79 | #if __GLASGOW_HASKELL__ >= 810 80 | feedParserText = bifoldMap fail return . parseOnly 81 | #else 82 | feedParserText p t = 83 | case parseOnly p t of 84 | Left s -> fail s 85 | Right s -> return s 86 | #endif 87 | 88 | type Recursive m b a = b -> (b, Parser (m a)) 89 | 90 | class (Foldable m) => ParseLinesTill m 91 | where 92 | -- | Fail and reset position when a breaker is found 93 | stop :: forall a. Parser (m a) -> Parser (Either () [a]) 94 | stop p' = hasMoreInput *> do 95 | z <- (Right <$> p') <> (return . Left) () 96 | case z of 97 | Left _ -> return (Left ()) 98 | Right x -> guard ((not . null) x) $> (Right . toList $ x) 99 | 100 | takeContent :: forall a b. Recursive m b a -> b -> Parser (Text, [a]) 101 | takeContent next c = tContent <> return (Text.empty, []) 102 | where 103 | (c', p) = next c 104 | tContent = do 105 | z <- stop p 106 | case z of 107 | Left _ -> (\l -> (Text.append l . fst) &&& snd) <$> takeALine <*> takeContent next c' 108 | Right as -> return (Text.empty, as) 109 | 110 | 111 | -- | Save the content and parse as the default Plain Text or default Section Paragraph 112 | -- and try to parse the new block if the new block exists under the same node 113 | parseLinesContextuallyTill :: forall a b. Parser a -> Recursive m b a -> b -> Parser [a] 114 | parseLinesContextuallyTill pD next c= skipEmptyLines *> hasMoreInput *> do 115 | (content, blocks ) <- takeContent next c 116 | guard (not $ Text.null content && null blocks) *> ((: blocks) <$> feedParserText pD content) 117 | 118 | parseLinesTill :: forall a. Parser a -> Parser (m a) -> Parser [a] 119 | parseLinesTill pDefault pBreaker = parseLinesContextuallyTill pDefault (const (0 :: Integer , pBreaker)) 0 120 | 121 | instance ParseLinesTill (Either a) 122 | 123 | skipEmptyLines :: Parser () 124 | skipEmptyLines = Attoparsec.Text.many' takeEmptyLine $> () 125 | -------------------------------------------------------------------------------- /orgmode-parse.cabal: -------------------------------------------------------------------------------- 1 | Name: orgmode-parse 2 | Version: 0.3.0 3 | Author: Parnell Springmeyer 4 | Maintainer: Parnell Springmeyer 5 | License: BSD3 6 | License-File: LICENSE 7 | bug-reports: https://github.com/ixmatus/orgmode-parse/issues 8 | Category: Data 9 | Synopsis: A collection of Attoparsec combinators for parsing org-mode 10 | flavored documents. 11 | Description: 12 | 13 | <> 14 | . 15 | `orgmode-parse` is a parsing library for the org-mode flavor of 16 | document markup. 17 | . 18 | The provided Attoparsec combinators parse the human-readable and 19 | textual representation into a simple AST. 20 | 21 | Cabal-Version: >= 1.10 22 | Build-Type: Simple 23 | 24 | Extra-Source-Files: 25 | LICENSE 26 | CHANGELOG.md 27 | README.md 28 | 29 | Library 30 | Default-Language: Haskell2010 31 | HS-Source-Dirs: src 32 | Ghc-options: 33 | -Wall -Werror -fwarn-tabs -funbox-strict-fields -fno-warn-orphans -fno-warn-unused-do-bind 34 | 35 | Exposed-Modules: 36 | Data.OrgMode.Parse, 37 | Data.OrgMode.Parse.Attoparsec.Constants, 38 | Data.OrgMode.Parse.Attoparsec.Content, 39 | Data.OrgMode.Parse.Attoparsec.Content.List, 40 | Data.OrgMode.Parse.Attoparsec.Content.Markup, 41 | Data.OrgMode.Parse.Attoparsec.Content.Paragraph, 42 | Data.OrgMode.Parse.Attoparsec.Document, 43 | Data.OrgMode.Parse.Attoparsec.Drawer, 44 | Data.OrgMode.Parse.Attoparsec.Drawer.Generic, 45 | Data.OrgMode.Parse.Attoparsec.Drawer.Logbook, 46 | Data.OrgMode.Parse.Attoparsec.Drawer.Property, 47 | Data.OrgMode.Parse.Attoparsec.Headline, 48 | Data.OrgMode.Parse.Attoparsec.Section, 49 | Data.OrgMode.Parse.Attoparsec.Time, 50 | Data.OrgMode.Parse.Attoparsec.Util, 51 | Data.OrgMode.Parse.Attoparsec.Util.ParseLinesTill, 52 | Data.OrgMode.Types 53 | 54 | Build-Depends: 55 | base >= 4.8 && < 5 56 | , aeson >= 1.0 57 | , attoparsec >= 0.13 58 | , bytestring >= 0.10.4 59 | , containers >= 0.5.6 60 | , free >= 4.9 61 | , hashable >= 1.2 62 | , insert-ordered-containers >= 0.2.0.0 63 | , old-locale >= 1.0 64 | , semigroups 65 | , text >= 1.2 66 | , thyme >= 0.3 67 | , unordered-containers >= 0.2.7 68 | 69 | Test-Suite tests 70 | Type: exitcode-stdio-1.0 71 | Default-Language: Haskell2010 72 | Hs-Source-Dirs: test 73 | Ghc-Options: -Wall -fwarn-tabs -funbox-strict-fields -fno-warn-orphans -fno-warn-unused-do-bind -fbreak-on-error 74 | Main-Is: Test.hs 75 | 76 | other-modules: 77 | Content.Contents 78 | , Content.List 79 | , Content.Paragraph 80 | , Document 81 | , Drawer 82 | , Headline 83 | , Timestamps 84 | , Util 85 | , Util.Builder 86 | , Weekdays 87 | 88 | Build-Depends: 89 | base >= 4.8 && < 5 90 | , HUnit >= 1.3 91 | , aeson >= 1.0 92 | , attoparsec >= 0.13 93 | , bytestring >= 0.10.4 94 | , containers >= 0.5.6 95 | , free >= 4.9 96 | , hashable >= 1.2 97 | , insert-ordered-containers >= 0.2.0.0 98 | , neat-interpolation >= 0.3 99 | , old-locale >= 1.0 100 | , orgmode-parse 101 | , semigroups 102 | , tasty >= 0.11 103 | , tasty-hunit >= 0.9 104 | , text >= 1.2 105 | , thyme >= 0.3 106 | , unordered-containers >= 0.2.7 107 | 108 | Source-Repository head 109 | Type: git 110 | Location: https://github.com/digitalmentat/orgmode-parse 111 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Content/Markup.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Data.OrgMode.Parse.Attoparsec.Content.Markup 4 | -- Copyright : © 2014 Parnell Springmeyer 5 | -- License : All Rights Reserved 6 | -- Maintainer : Parnell Springmeyer 7 | -- Stability : stable 8 | -- 9 | -- Parsing combinators for org-mode markup and paragraphs. 10 | ---------------------------------------------------------------------------- 11 | 12 | {-# LANGUAGE CPP #-} 13 | {-# LANGUAGE OverloadedStrings #-} 14 | {-# LANGUAGE RecordWildCards #-} 15 | 16 | module Data.OrgMode.Parse.Attoparsec.Content.Markup 17 | ( parseMarkupContent 18 | , parsePlainText 19 | ) 20 | where 21 | 22 | import Control.Applicative (optional) 23 | import Data.Semigroup ((<>)) 24 | #if __GLASGOW_HASKELL__ >= 810 25 | import Data.Bifoldable (bifoldMap) 26 | #endif 27 | import Data.Attoparsec.Text (Parser, anyChar, char, choice, 28 | endOfInput, isEndOfLine, manyTill, 29 | parseOnly, skipSpace, takeWhile) 30 | import Data.Char (isSpace) 31 | import Data.Text (Text, append, cons, dropWhileEnd, 32 | intercalate, snoc, strip, stripEnd) 33 | import Prelude hiding (takeWhile) 34 | 35 | import Data.OrgMode.Types (MarkupText (..)) 36 | 37 | import qualified Data.Text as Text 38 | 39 | data Token = Token 40 | { keyChar :: Char 41 | , markup :: [MarkupText] -> MarkupText 42 | } 43 | 44 | -- | A set of token definitions for markup keywords 45 | tokens :: [Token] 46 | tokens = 47 | [ Token '*' Bold 48 | , Token '_' UnderLine 49 | , Token '/' Italic 50 | , Token '+' Strikethrough 51 | ] 52 | 53 | -- | For better efficiency suggested by Attoparsec, we hard code the 54 | -- token filter. 55 | isNotToken :: Char -> Bool 56 | isNotToken c = c `notElem` tokenKeywods 57 | where 58 | tokenKeywods = ['$', '=', '~'] ++ map keyChar tokens 59 | 60 | -- | A parser for hyper-link markup. 61 | parseHyperLink :: Parser MarkupText 62 | parseHyperLink = do 63 | _ <- char '[' 64 | 65 | link <- parseLink 66 | description <- optional parseDescription 67 | 68 | _ <- char ']' 69 | 70 | pure HyperLink{..} 71 | 72 | where 73 | parseLink = parseBracketText 74 | parseDescription = parseBracketText 75 | parseBracketText = char '[' *> takeWhile (/= ']') <* char ']' 76 | 77 | -- | A Naive parser for LaTeX 78 | parseLaTeX :: Parser MarkupText 79 | parseLaTeX = char '$' *> (LaTeX <$> parseL) 80 | where 81 | parseL = do 82 | content <- takeWhile (/= '$') 83 | if Text.last content /= '\\' 84 | then return content 85 | else append content <$> parseL 86 | 87 | -- | Create Naive Parser for Code and Verbatim marked-up content. 88 | -- 89 | -- This cannot serve LaTeX because LaTeX code may include \$ in LaTeX 90 | -- block. 91 | parseVerbatimLike :: Char -> (Text -> MarkupText) -> Parser MarkupText 92 | parseVerbatimLike c m = char c *> (m <$> parseL) 93 | where 94 | parseL = takeWhile (/= c) <* char c 95 | 96 | parseVerbatim :: Parser MarkupText 97 | parseVerbatim = parseVerbatimLike '=' Verbatim 98 | 99 | parseCode :: Parser MarkupText 100 | parseCode = parseVerbatimLike '~' Code 101 | 102 | -- | Create a markup parser based on a token. 103 | createTokenParser :: Parser [MarkupText] -> Token -> Parser MarkupText 104 | createTokenParser innerParser Token{..} = do 105 | -- Spaces just after the spaces 106 | _ <- char keyChar <* skipSpace 107 | content <- takeWhile (/= keyChar) 108 | _ <- char keyChar 109 | -- We need another parser passed in to parse the markup inside the markup 110 | #if __GLASGOW_HASKELL__ >= 810 111 | bifoldMap fail (return . markup) (parseOnly innerParser content) 112 | #else 113 | case parseOnly innerParser content of 114 | Left s -> fail s 115 | Right a -> return (markup a) 116 | #endif 117 | 118 | -- | The fallback default if all markup parsers fail. 119 | parsePlainText :: Parser MarkupText 120 | parsePlainText = do 121 | c <- anyChar 122 | -- Append the first char and then refactor all spaces at line end 123 | -- and line beginning 124 | content <- adaptSpace . cons c <$> takeWhile isNotToken 125 | return $ Plain content 126 | 127 | -- | Take the line break as common space. 128 | -- 129 | -- 1. spaces before "\n" shall be omitted 130 | -- 2. spaces after "\n" shall be omitted 131 | -- 3. "\n" shall be considered as simple " " 132 | adaptSpace :: Text -> Text 133 | adaptSpace str = fix content 134 | where 135 | textLines = 136 | case Text.split isEndOfLine str of 137 | [] -> [] 138 | (firstLine : restLines) -> dropWhileEnd isSpace firstLine : map strip restLines 139 | 140 | content = intercalate (Text.pack " ") textLines 141 | 142 | fix s | isSpace (Text.last str) = snoc s ' ' 143 | | otherwise = s 144 | 145 | -- | Normalize to a concise Markup Array after serially running 146 | -- parsers. 147 | -- 148 | -- 1. Concat the Neighbour Plain Texts 149 | -- 2. Remove empty Plain Text 150 | -- 3. Remove the Plain spaces at the end 151 | appendElement :: MarkupText -> [MarkupText] -> [MarkupText] 152 | appendElement (Plain t) [] 153 | -- Remove the spaces by the end of paragraph or of markup 154 | | strip t == "" 155 | = [] 156 | | otherwise 157 | = [Plain (stripEnd t)] 158 | 159 | appendElement a [] = [a] 160 | appendElement (Plain text1) (Plain text2: xs) 161 | | Text.null text1 && Text.null text2 162 | = xs 163 | | otherwise 164 | = Plain (append text1 text2) : xs 165 | appendElement h t 166 | | h == Plain Text.empty 167 | = t 168 | | head t == Plain Text.empty 169 | = h: tail t 170 | | otherwise 171 | = h:t 172 | 173 | -- | Parse the whole text content to an array of Markup Text. 174 | -- 175 | -- This parser will not handle the block stop. The block stop shall be 176 | -- already handled before passing text with this Parser 177 | parseMarkupContent :: Parser [MarkupText] 178 | parseMarkupContent = foldr appendElement [] <$> manyTill parseMarkup (skipSpace *> endOfInput) 179 | where 180 | parseMarkup :: Parser MarkupText 181 | parseMarkup = 182 | choice (map (createTokenParser parseMarkupContent) tokens) <> 183 | choice [ parseHyperLink, parseLaTeX, parseVerbatim, parseCode, parsePlainText ] 184 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Headline.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Data.OrgMode.Parse.Attoparsec.Headline 4 | -- Copyright : © 2014 Parnell Springmeyer 5 | -- License : All Rights Reserved 6 | -- Maintainer : Parnell Springmeyer 7 | -- Stability : stable 8 | -- 9 | -- Parsing combinators for org-mode headlines. 10 | ---------------------------------------------------------------------------- 11 | 12 | 13 | {-# LANGUAGE DuplicateRecordFields #-} 14 | {-# LANGUAGE OverloadedStrings #-} 15 | {-# LANGUAGE ViewPatterns #-} 16 | 17 | module Data.OrgMode.Parse.Attoparsec.Headline 18 | ( headlineBelowDepth 19 | , headlineDepth 20 | , headingPriority 21 | , parseStats 22 | , parseTags 23 | , mkTitleMeta 24 | , TitleMeta 25 | ) 26 | where 27 | 28 | import Control.Applicative 29 | import Data.Attoparsec.Text 30 | import Data.Attoparsec.Types as Attoparsec (Parser) 31 | import Data.Maybe 32 | import Data.Monoid 33 | import Data.Text (Text) 34 | import qualified Data.Text as Text 35 | import GHC.Natural (Natural) 36 | import Prelude hiding (takeWhile) 37 | import Text.Printf 38 | 39 | import Data.Functor (($>)) 40 | import Data.OrgMode.Parse.Attoparsec.Section 41 | import qualified Data.OrgMode.Parse.Attoparsec.Time as OrgMode.Time 42 | import Data.OrgMode.Parse.Attoparsec.Util 43 | import Data.OrgMode.Types 44 | 45 | -- | Intermediate type for parsing titles in a headline after the 46 | -- state keyword and priority have been parsed. 47 | type Tag = Text 48 | 49 | newtype TitleMeta = TitleMeta (Text, Maybe Stats, Maybe [Tag]) 50 | deriving (Eq, Show) 51 | 52 | -- | Parse an org-mode headline, its metadata, its section-body, and 53 | -- any sub-headlines; please see 54 | -- . 55 | -- 56 | -- Headline metadata includes a hierarchy level indicated by 57 | -- asterisks, optional todo state keywords, an optional priority 58 | -- level, %-done statistics, and tags; e.g: 59 | -- 60 | -- > ** TODO [#B] Polish Poetry Essay [25%] :HOMEWORK:POLISH:WRITING: 61 | -- 62 | -- Headlines may contain: 63 | -- 64 | -- - A section with Planning and Clock entries 65 | -- - A number of other entities (code blocks, lists) 66 | -- - Unstructured text 67 | -- - Sub-headlines 68 | -- 69 | -- @headlineBelowDepth@ takes a list of terms to consider, state 70 | -- keywords, and a minumum hierarchy depth. 71 | -- 72 | -- Use a @Depth@ of 0 to parse any headline. 73 | headlineBelowDepth :: [Text] 74 | -> Depth 75 | -> Attoparsec.Parser Text Headline 76 | headlineBelowDepth stateKeywords d = do 77 | depth' <- headlineDepth d <* skipOnlySpace 78 | stateKey <- option Nothing (Just <$> parseStateKeyword stateKeywords <* skipOnlySpace) 79 | priority' <- option Nothing (Just <$> headingPriority <* skipOnlySpace) 80 | tstamp <- option Nothing (Just <$> OrgMode.Time.parseTimestamp <* skipOnlySpace) 81 | 82 | -- Parse the title and any metadata within it 83 | TitleMeta 84 | ( titleText 85 | , stats' 86 | , fromMaybe [] -> tags' 87 | ) <- parseTitle 88 | 89 | section' <- parseSection 90 | subHeadlines' <- option [] $ many' (headlineBelowDepth stateKeywords (d + 1)) 91 | 92 | skipSpace 93 | pure $ Headline 94 | { depth = depth' 95 | , stateKeyword = stateKey 96 | , priority = priority' 97 | , title = titleText 98 | , timestamp = tstamp 99 | , stats = stats' 100 | , tags = tags' 101 | , section = section' 102 | , subHeadlines = subHeadlines' 103 | } 104 | 105 | -- | Parse the asterisk-indicated headline depth until a space is 106 | -- encountered. 107 | -- 108 | -- Constrain it to @Depth@. 109 | headlineDepth :: Depth -> Attoparsec.Parser Text Depth 110 | headlineDepth (Depth d) = takeDepth >>= test 111 | where 112 | takeDepth = fromIntegral . Text.length <$> takeWhile1 (== '*') 113 | 114 | test :: Natural -> Attoparsec.Parser Text Depth 115 | test n | n <= d = fail (printf "Headline depth of %d cannot be higher than a depth constraint of %d" n d) 116 | | otherwise = pure (Depth n) 117 | 118 | -- | Parse the state indicator. 119 | -- 120 | -- > {`TODO` | `DONE` | custom } 121 | -- 122 | -- These can be custom so we're parsing additional state identifiers 123 | -- as Text. 124 | parseStateKeyword :: [Text] -> Attoparsec.Parser Text StateKeyword 125 | parseStateKeyword (fmap string -> sk) = StateKeyword <$> choice sk 126 | 127 | -- | Parse the priority indicator. 128 | -- 129 | -- If anything but these priority indicators are used the parser will 130 | -- fail: 131 | -- 132 | -- - @[#A]@ 133 | -- - @[#B]@ 134 | -- - @[#C]@ 135 | headingPriority :: Attoparsec.Parser Text Priority 136 | headingPriority = start *> zipChoice <* end 137 | where 138 | zipChoice = choice (zipWith mkPParser "ABC" [A,B,C]) 139 | mkPParser c p = char c $> p 140 | start = string "[#" 141 | end = char ']' 142 | 143 | -- | Parse the title, optional stats block, and optional tag. 144 | -- 145 | -- Stats may be either [m/n] or [n%] and tags are colon-separated, e.g: 146 | -- > :HOMEWORK:POETRY:WRITING: 147 | parseTitle :: Attoparsec.Parser Text TitleMeta 148 | parseTitle = 149 | mkTitleMeta <$> 150 | titleStart <*> 151 | optMeta parseStats <*> 152 | optMeta parseTags <*> 153 | -- Parse what's leftover AND till end of line or input; discarding 154 | -- everything but the leftovers 155 | leftovers <* (endOfLine <|> endOfInput) 156 | where 157 | titleStart = takeTill (\c -> inClass "[:" c || isEndOfLine c) 158 | leftovers = option mempty $ takeTill (== '\n') 159 | optMeta p = option Nothing (Just <$> p <* skipOnlySpace) 160 | 161 | -- | Produce a triple consisting of a stripped start-of-title if there 162 | -- are no leftovers after parsing (otherwise, recombine the two) and 163 | -- the optional stats and tags. 164 | mkTitleMeta :: Text -- ^ Start of title till the end of line 165 | -> Maybe Stats -- ^ Stats, e.g: [33%] 166 | -> Maybe [Tag] -- ^ Tags, e.g: :HOMEWORK:CODE:SLEEP: 167 | -> Text -- ^ Leftovers (may be empty) of the title 168 | -> TitleMeta 169 | mkTitleMeta start stats' tags' leftovers = 170 | TitleMeta (cleanTitle start leftovers, stats', tags') 171 | where 172 | cleanTitle t l 173 | | Text.null leftovers = Text.strip t 174 | | otherwise = Text.append t l 175 | 176 | -- | Parse a statisticss block, e.g: [33%]. 177 | -- 178 | -- Accepts either form: "[m/n]" or "[n%]" and there is no restriction 179 | -- on m or n other than that they are integers. 180 | parseStats :: Attoparsec.Parser Text Stats 181 | parseStats = pct <|> frac 182 | where 183 | pct = StatsPct 184 | <$> (char '[' *> decimal <* string "%]") 185 | frac = StatsOf 186 | <$> (char '[' *> decimal) 187 | <*> (char '/' *> decimal <* char ']') 188 | 189 | -- | Parse a colon-separated list of tags. 190 | -- 191 | -- > :HOMEWORK:POETRY:WRITING: 192 | parseTags :: Attoparsec.Parser Text [Tag] 193 | parseTags = tags' >>= test 194 | where 195 | tags' = char ':' *> takeWhile (/= '\n') 196 | test t 197 | | Text.null t = fail "no data after beginning ':'" 198 | | Text.last t /= ':' = fail $ Text.unpack $ "expected ':' at end of tag list but got: " `Text.snoc` Text.last t 199 | | Text.length t < 2 = fail $ Text.unpack $ "not a valid tag set: " <> t 200 | | otherwise = pure (Text.splitOn ":" (Text.init t)) 201 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Parse/Attoparsec/Time.hs: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- | 3 | -- Module : Data.OrgMode.Parse.Attoparsec.Time 4 | -- Copyright : © 2014 Parnell Springmeyer 5 | -- License : All Rights Reserved 6 | -- Maintainer : Parnell Springmeyer 7 | -- Stability : stable 8 | -- 9 | -- Parsing combinators for org-mode timestamps; both active and 10 | -- inactive. 11 | ---------------------------------------------------------------------------- 12 | 13 | {-# LANGUAGE DataKinds #-} 14 | {-# LANGUAGE OverloadedStrings #-} 15 | {-# LANGUAGE RecordWildCards #-} 16 | {-# LANGUAGE ViewPatterns #-} 17 | 18 | module Data.OrgMode.Parse.Attoparsec.Time 19 | ( parsePlannings 20 | , parseClock 21 | , parseTimestamp 22 | ) 23 | where 24 | 25 | import Control.Applicative 26 | import Data.Attoparsec.Combinator as Attoparsec 27 | import Data.Attoparsec.Text 28 | import Data.Attoparsec.Types as Attoparsec (Parser) 29 | import Data.Functor (($>)) 30 | import Data.Maybe (listToMaybe) 31 | import Data.Semigroup ((<>)) 32 | import Data.Text (Text) 33 | import Data.Thyme.Format (buildTime, timeParser) 34 | import Data.Thyme.LocalTime (Hours, Minutes) 35 | import Prelude hiding (repeat) 36 | import System.Locale (defaultTimeLocale) 37 | 38 | import Data.OrgMode.Types 39 | import qualified Data.OrgMode.Parse.Attoparsec.Util as Util 40 | 41 | import qualified Data.Attoparsec.ByteString as Attoparsec.ByteString 42 | import qualified Data.ByteString.Char8 as BS 43 | import qualified Data.Text as Text 44 | 45 | -- | Parse a planning line. 46 | -- 47 | -- Plannings inhabit a heading section and are formatted as a keyword 48 | -- and a timestamp. There can be more than one, but they are all on 49 | -- the same line e.g: 50 | -- 51 | -- > DEADLINE: <2015-05-10 17:00> CLOSED: <2015-04-1612:00> 52 | parsePlannings :: Attoparsec.Parser Text [Planning] 53 | parsePlannings = many' (skipSpace *> planning <* Util.skipOnlySpace) 54 | where 55 | planning = Planning <$> keyword <* char ':' <*> (skipSpace *> parseTimestamp) 56 | keyword = 57 | choice [ string "SCHEDULED" $> SCHEDULED 58 | , string "DEADLINE" $> DEADLINE 59 | , string "CLOSED" $> CLOSED 60 | ] 61 | 62 | -- | Parse a clock line. 63 | -- 64 | -- A heading's section contains one line per clock entry. Clocks may 65 | -- have a timestamp, a duration, both, or neither e.g.: 66 | -- 67 | -- > CLOCK: [2014-12-10 Fri 2:30]--[2014-12-10 Fri 10:30] => 08:00 68 | parseClock :: Attoparsec.Parser Text Clock 69 | parseClock = Clock <$> ((,) <$> (skipSpace *> string "CLOCK: " *> ts) <*> dur) 70 | where 71 | ts = optional parseTimestamp 72 | dur = optional (string " => " *> skipSpace *> parseHM) 73 | 74 | -- | Parse a timestamp. 75 | -- 76 | -- Timestamps may be timepoints or timeranges, and they indicate 77 | -- whether they are active or closed by using angle or square brackets 78 | -- respectively. 79 | -- 80 | -- Time ranges are formatted by infixing two timepoints with a double 81 | -- hyphen, @--@; or, by appending two @hh:mm@ timestamps together in a 82 | -- single timepoint with one hyphen @-@. 83 | -- 84 | -- Each timepoint includes an optional repeater flag and an optional 85 | -- delay flag. 86 | parseTimestamp :: Attoparsec.Parser Text Timestamp 87 | parseTimestamp = do 88 | (ts1, tsb1, act) <- transformBracketedDateTime <$> parseBracketedDateTime 89 | 90 | blk2 <- fmap (fmap transformBracketedDateTime) optionalBracketedDateTime 91 | 92 | -- TODO: refactor this case logic 93 | case (tsb1, blk2) of 94 | (Nothing, Nothing) -> 95 | pure (Timestamp ts1 act Nothing) 96 | (Nothing, Just (ts2, Nothing, _)) -> 97 | pure (Timestamp ts1 act (Just ts2)) 98 | (Nothing, Just _) -> 99 | -- TODO: improve error message with an example of what would 100 | -- cause this case 101 | fail "Illegal time range in second timerange timestamp" 102 | (Just (h',m'), Nothing) -> 103 | pure (Timestamp ts1 act 104 | (Just $ ts1 {hourMinute = Just (h',m') 105 | ,repeater = Nothing 106 | ,delay = Nothing})) 107 | (Just _, Just _) -> 108 | -- TODO: improve error message with an example of what would 109 | -- cause thise case 110 | fail "Illegal mix of time range and timestamp range" 111 | 112 | where 113 | optionalBracketedDateTime = 114 | optional (string "--" *> parseBracketedDateTime) 115 | 116 | 117 | -- | Parse a single time part. 118 | -- 119 | -- > [2015-03-27 Fri 10:20 +4h] 120 | -- 121 | -- Returns: 122 | -- 123 | -- - The basic timestamp 124 | -- - Whether there was a time interval in place of a single time 125 | -- (this will be handled upstream by parseTimestamp) 126 | -- - Whether the time is active or inactive 127 | parseBracketedDateTime :: Attoparsec.Parser Text BracketedDateTime 128 | parseBracketedDateTime = do 129 | openingBracket <- char '<' <|> char '[' 130 | brkDateTime <- BracketedDateTime <$> 131 | parseDate <* skipSpace 132 | <*> optionalParse parseDay 133 | <*> optionalParse parseTime' 134 | <*> maybeListParse parseRepeater 135 | <*> maybeListParse parseDelay 136 | <*> pure (activeBracket openingBracket) 137 | 138 | closingBracket <- char '>' <|> char ']' 139 | finally brkDateTime openingBracket closingBracket 140 | where 141 | optionalParse p = optional p <* skipSpace 142 | maybeListParse p = listToMaybe <$> many' p <* skipSpace 143 | activeBracket ((=='<') -> active) = 144 | if active then Active else Inactive 145 | 146 | finally bkd ob cb | complementaryBracket ob /= cb = 147 | -- TODO: improve this error message with an 148 | -- example of what would cause this case 149 | fail "mismatched timestamp brackets" 150 | | otherwise = return bkd 151 | 152 | complementaryBracket '<' = '>' 153 | complementaryBracket '[' = ']' 154 | complementaryBracket x = x 155 | 156 | -- | Given a @BracketedDateTime@ data type, transform it into a triple 157 | -- composed of a @DateTime@, possibly a @(Hours, Minutes)@ tuple 158 | -- signifying the end of a timestamp range, and a boolean indic 159 | transformBracketedDateTime :: BracketedDateTime 160 | -> (DateTime, Maybe (Hours, Minutes), ActiveState) 161 | transformBracketedDateTime BracketedDateTime{..} = 162 | maybe dateStamp timeStamp timePart 163 | where 164 | defdt = DateTime datePart dayNamePart Nothing repeat delayPart 165 | timeStamp (AbsoluteTime (hs,ms)) = 166 | ( defdt { hourMinute = Just (hs,ms) } 167 | , Nothing 168 | , activeState 169 | ) 170 | timeStamp (TimeStampRange (t0,t1)) = 171 | ( defdt { hourMinute = Just t0 } 172 | , Just t1 173 | , activeState 174 | ) 175 | dateStamp = (defdt, Nothing, activeState) 176 | 177 | 178 | -- | Parse a day name in the same way as org-mode does. 179 | -- 180 | -- The character set (@]+0123456789>\r\n -@) is based on a part of a 181 | -- regexp named @org-ts-regexp0@ found in org.el. 182 | parseDay :: Attoparsec.Parser Text Text 183 | parseDay = Text.pack <$> some (Attoparsec.satisfyElem isDayChar) 184 | where 185 | isDayChar :: Char -> Bool 186 | isDayChar = (`notElem` nonDayChars) 187 | 188 | -- | This is based on: @[^]+0-9>\r\n -]+@, a part of a regexp 189 | -- named org-ts-regexp0 in org.el. 190 | nonDayChars = "]+0123456789>\r\n -" :: String 191 | 192 | -- | Parse the time-of-day part of a time part, as a single point or a 193 | -- time range. 194 | parseTime' :: Attoparsec.Parser Text TimePart 195 | parseTime' = stampRng <|> stampAbs 196 | where 197 | stampRng = do 198 | beg <- parseHM <* char '-' 199 | end <- parseHM 200 | pure $ TimeStampRange (beg,end) 201 | 202 | stampAbs = AbsoluteTime <$> parseHM 203 | 204 | -- | Parse the YYYY-MM-DD part of a time part. 205 | parseDate :: Attoparsec.Parser Text YearMonthDay 206 | parseDate = consumeDate >>= either bad good . dateParse 207 | where 208 | bad e = fail ("failure parsing date: " <> e) 209 | good t = pure (buildTime t) 210 | consumeDate = manyTill anyChar (char ' ') 211 | dateParse = Attoparsec.ByteString.parseOnly dpCombinator . BS.pack 212 | dpCombinator = timeParser defaultTimeLocale "%Y-%m-%d" 213 | 214 | -- | Parse a single @HH:MM@ point. 215 | parseHM :: Attoparsec.Parser Text (Hours, Minutes) 216 | parseHM = (,) <$> decimal <* char ':' <*> decimal 217 | 218 | -- | Parse the Timeunit part of a delay or repeater flag. 219 | parseTimeUnit :: Attoparsec.Parser Text TimeUnit 220 | parseTimeUnit = 221 | choice [ char 'h' $> UnitHour 222 | , char 'd' $> UnitDay 223 | , char 'w' $> UnitWeek 224 | , char 'm' $> UnitMonth 225 | , char 'y' $> UnitYear 226 | ] 227 | 228 | -- | Parse a repeater flag, e.g. @.+4w@, or @++1y@. 229 | parseRepeater :: Attoparsec.Parser Text Repeater 230 | parseRepeater = 231 | Repeater 232 | <$> choice 233 | [ string "++" $> RepeatCumulate 234 | , char '+' $> RepeatCatchUp 235 | , string ".+" $> RepeatRestart 236 | ] 237 | <*> decimal 238 | <*> parseTimeUnit 239 | 240 | -- | Parse a delay flag, e.g. @--1d@ or @-2w@. 241 | parseDelay :: Attoparsec.Parser Text Delay 242 | parseDelay = 243 | Delay 244 | <$> choice 245 | [ string "--" $> DelayFirst 246 | , char '-' $> DelayAll 247 | ] 248 | <*> decimal 249 | <*> parseTimeUnit 250 | -------------------------------------------------------------------------------- /test/Document.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DuplicateRecordFields #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Document where 5 | 6 | import Data.Attoparsec.Text 7 | import Data.HashMap.Strict.InsOrd hiding (map) 8 | import Data.OrgMode.Parse.Attoparsec.Document 9 | import Data.OrgMode.Parse.Attoparsec.Time 10 | import Data.OrgMode.Types 11 | import Data.Text hiding (map) 12 | import Test.Tasty 13 | import Test.Tasty.HUnit 14 | import Util 15 | import Util.Builder 16 | 17 | import qualified Data.Text as Text 18 | import qualified Data.Text.IO as TextIO 19 | import qualified Control.Applicative as Applicative 20 | 21 | parserSmallDocumentTests :: TestTree 22 | parserSmallDocumentTests = testGroup "Attoparsec Small Document" 23 | [ testCase "Parse Empty Document" $ 24 | testDocS "" (Document "" []) 25 | 26 | , testCase "Parse No Headline" $ 27 | testDocS pText (Document pText []) 28 | 29 | , testCase "Parse Headline Sample A" $ 30 | testDocS sampleAText sampleAParse 31 | 32 | , testCase "Parse Headline with Planning" $ 33 | testDocS samplePText samplePParse 34 | 35 | , testCase "Parse Headline with properties and sublist" $ 36 | testDocS sampleP2Text sampleP2Parse 37 | 38 | , testCase "Parse Headline with scheduled and sublist" $ 39 | testDocS sampleP3Text sampleP3Parse 40 | 41 | , testCase "Parse Headline no \n" $ 42 | testDocS "* T" (Document "" [emptyHeadline {title="T"}]) 43 | 44 | , testCase "Parse Document from File" 45 | testDocFile 46 | 47 | , testCase "Parse Document with Subtree List Items" 48 | testSubtreeListItemDocFile 49 | ] 50 | 51 | where 52 | testDocS s r = expectParse parseDocument s (Right r) 53 | 54 | testDocFile = do 55 | doc <- TextIO.readFile "test/docs/test-document.org" 56 | 57 | let testDoc = parseOnly parseDocument doc 58 | 59 | assertBool "Expected to parse document" (parseSucceeded testDoc) 60 | 61 | testSubtreeListItemDocFile = do 62 | doc <- TextIO.readFile "test/docs/subtree-list-items.org" 63 | 64 | -- let subtreeListItemsDoc = parseOnly (parseDocument []) doc 65 | 66 | -- assertBool "Expected to parse document" (subtreeListItemsDoc == goldenSubtreeListItemDoc) 67 | expectParse (parseDocumentWithKeywords []) doc goldenSubtreeListItemDoc 68 | 69 | pText = "Paragraph text\n.No headline here.\n##--------\n" 70 | parseSucceeded (Right _) = True 71 | parseSucceeded (Left _ ) = False 72 | 73 | sampleAText :: Text 74 | sampleAText = Text.concat [sampleParagraph,"* Test1", spaces 20,":Hi there:\n" 75 | ,"*\n" 76 | ," *\n" 77 | ,"* Test2 :Two:Tags:\n" 78 | ] 79 | sampleAParse :: Document 80 | sampleAParse = Document 81 | sampleParagraph 82 | -- Headline shall have space after * 83 | [emptyHeadline {title="Test1", tags=["Hi there"], section = emptySection {sectionContents= [toP (Bold [])]}} 84 | ,emptyHeadline {title="Test2", tags=["Two","Tags"]} 85 | ] 86 | 87 | samplePText :: Text 88 | samplePText = Text.concat ["* Test3\n" 89 | ," SCHEDULED: <2015-06-12 Fri>" 90 | ] 91 | 92 | samplePParse :: Document 93 | samplePParse = Document "" [ emptyHeadline { title="Test3", section = sect } ] 94 | where 95 | sect = emptySection{ sectionPlannings = plannings } 96 | where 97 | Right plannings = parseOnly parsePlannings "SCHEDULED: <2015-06-12 Fri>" 98 | 99 | sampleP2Text :: Text 100 | sampleP2Text = 101 | Text.concat ["* Test3_1\n" 102 | ," :PROPERTIES:\n" 103 | ," :CATEGORY: testCategory\n" 104 | ," :END:\n" 105 | ," * One bullet list element\n" 106 | ,"* Test3_2\n" 107 | ] 108 | 109 | sampleP2Parse :: Document 110 | sampleP2Parse = 111 | Document "" [ emptyHeadline { 112 | title = "Test3_1" 113 | , section = emptySection { 114 | sectionProperties = Properties (fromList [("CATEGORY", "testCategory")]) 115 | , sectionContents = [UnorderedList [Item [Paragraph [Plain "One bullet list element"]]]]}} 116 | , emptyHeadline { title = "Test3_2"}] 117 | 118 | sampleP3Text :: Text 119 | sampleP3Text = 120 | Text.concat ["* Test4_1\n" 121 | ," SCHEDULED: <2004-02-29 Sun 10:20>\n" 122 | ," * One bullet list element\n" 123 | ,"* Test4_2\n" 124 | ] 125 | 126 | sampleP3Parse :: Document 127 | sampleP3Parse = 128 | Document "" [ emptyHeadline { 129 | title = "Test4_1" 130 | , section = emptySection { 131 | sectionPlannings = [Planning SCHEDULED curTimestamp] 132 | , sectionContents = [UnorderedList [Item [Paragraph [Plain "One bullet list element"]]]]}} 133 | , emptyHeadline { title = "Test4_2"}] 134 | where 135 | curTimestamp = 136 | Timestamp 137 | (DateTime 138 | (YearMonthDay 2004 2 29) 139 | (Just "Sun") 140 | (Just (10,20)) 141 | Nothing Nothing 142 | ) 143 | Active 144 | Nothing 145 | 146 | 147 | emptyHeadline :: Headline 148 | emptyHeadline = 149 | Headline 150 | { depth = 1 151 | , stateKeyword = Nothing 152 | , priority = Nothing 153 | , title = "" 154 | , stats = Nothing 155 | , timestamp = Nothing 156 | , tags = [] 157 | , section = emptySection 158 | , subHeadlines = [] 159 | } 160 | 161 | sampleParagraph :: Text 162 | sampleParagraph = "This is some sample text in a paragraph which may contain * , : , and other special characters.\n\n" 163 | 164 | spaces :: Int -> Text 165 | spaces = flip Text.replicate " " 166 | 167 | emptySection :: Section 168 | emptySection = Section Nothing mempty mempty mempty mempty mempty 169 | 170 | plainParagraphs :: Text -> [Content] 171 | plainParagraphs str = [Paragraph [Plain str]] 172 | 173 | goldenSubtreeListItemDoc :: Either String Document 174 | goldenSubtreeListItemDoc = 175 | Right 176 | (Document 177 | { documentText = "" 178 | , documentHeadlines = [ 179 | Headline 180 | { depth = Depth 1 181 | , stateKeyword = Applicative.empty 182 | , priority = Applicative.empty 183 | , title = "Header1" 184 | , timestamp = Applicative.empty 185 | , stats = Applicative.empty 186 | , tags = Applicative.empty 187 | , section = 188 | Section 189 | { sectionTimestamp = Applicative.empty 190 | , sectionPlannings = Applicative.empty 191 | , sectionClocks = Applicative.empty 192 | , sectionProperties = Properties mempty 193 | , sectionLogbook = Logbook Applicative.empty 194 | , sectionContents = Applicative.empty 195 | } 196 | , subHeadlines = [ 197 | Headline 198 | { depth = Depth 2 199 | , stateKeyword = Applicative.empty 200 | , priority = Applicative.empty 201 | , title = "Header2" 202 | , timestamp = Applicative.empty 203 | , stats = Applicative.empty 204 | , tags = Applicative.empty 205 | , section = 206 | Section 207 | { sectionTimestamp = Applicative.empty 208 | , sectionPlannings = Applicative.empty 209 | , sectionClocks = Applicative.empty 210 | , sectionProperties = Properties mempty 211 | , sectionLogbook = Logbook Applicative.empty 212 | , sectionContents = Applicative.empty 213 | } 214 | , subHeadlines = [ 215 | Headline 216 | { depth = Depth 3 217 | , stateKeyword = Applicative.empty 218 | , priority = Applicative.empty 219 | , title = "Header3" 220 | , timestamp = Applicative.empty 221 | , stats = Applicative.empty 222 | , tags = Applicative.empty 223 | , subHeadlines = Applicative.empty 224 | , section = 225 | Section 226 | { sectionTimestamp = Applicative.empty 227 | , sectionPlannings = Applicative.empty 228 | , sectionClocks = Applicative.empty 229 | , sectionProperties = Properties { unProperties = fromList [("ONE", "two")] } 230 | , sectionLogbook = Logbook Applicative.empty 231 | , sectionContents = [ UnorderedList $ map toI [pack "Item1", pack "Item2"] ] 232 | } 233 | } 234 | ] 235 | } 236 | , Headline 237 | { depth = Depth 2 238 | , stateKeyword = Applicative.empty 239 | , priority = Applicative.empty 240 | , title = "Header4" 241 | , timestamp = Applicative.empty 242 | , stats = Applicative.empty 243 | , tags = Applicative.empty 244 | , subHeadlines = Applicative.empty 245 | , section = 246 | Section 247 | { sectionTimestamp = Applicative.empty 248 | , sectionPlannings = Applicative.empty 249 | , sectionClocks = Applicative.empty 250 | , sectionProperties = Properties mempty 251 | , sectionLogbook = Logbook Applicative.empty 252 | , sectionContents = Applicative.empty 253 | } 254 | } 255 | ] 256 | } 257 | ]}) 258 | -------------------------------------------------------------------------------- /src/Data/OrgMode/Types.hs: -------------------------------------------------------------------------------- 1 | {-| 2 | Module : Data.OrgMode.Types 3 | Copyright : © 2014 Parnell Springmeyer 4 | License : All Rights Reserved 5 | Maintainer : Parnell Springmeyer 6 | Stability : experimental 7 | 8 | Types for the AST of an org-mode document. 9 | -} 10 | 11 | {-# LANGUAGE CPP #-} 12 | {-# LANGUAGE DataKinds #-} 13 | {-# LANGUAGE DeriveGeneric #-} 14 | {-# LANGUAGE DeriveDataTypeable #-} 15 | {-# LANGUAGE DisambiguateRecordFields #-} 16 | {-# LANGUAGE DuplicateRecordFields #-} 17 | {-# LANGUAGE FlexibleInstances #-} 18 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 19 | {-# LANGUAGE OverloadedStrings #-} 20 | 21 | {-# OPTIONS -fno-warn-orphans #-} 22 | 23 | module Data.OrgMode.Types 24 | ( ActiveState (..) 25 | , BracketedDateTime (..) 26 | , Clock (..) 27 | , DateTime (..) 28 | , Delay (..) 29 | , DelayType (..) 30 | , Depth (..) 31 | , Document (..) 32 | , Drawer 33 | , Duration 34 | , Headline (..) 35 | , Logbook (..) 36 | , PlanningKeyword (..) 37 | , Planning (..) 38 | , Priority (..) 39 | , Properties (..) 40 | , Repeater (..) 41 | , RepeaterType (..) 42 | , Section (..) 43 | , StateKeyword (..) 44 | , Stats (..) 45 | , TimePart (..) 46 | , TimeUnit (..) 47 | , Timestamp (..) 48 | , YearMonthDay (..) 49 | , Content (..) 50 | , MarkupText (..) 51 | , Item (..) 52 | , sectionDrawer 53 | ) where 54 | 55 | import Control.Monad (mzero) 56 | import Data.Aeson (FromJSON (..), ToJSON (..), 57 | Value (..), defaultOptions, 58 | genericToEncoding, object, (.:), 59 | (.=)) 60 | import Data.Data (Data(..), Typeable) 61 | import Data.HashMap.Strict.InsOrd (InsOrdHashMap) 62 | import Data.Semigroup (Semigroup) 63 | import Data.Text (Text) 64 | import Data.Thyme.Calendar (YearMonthDay (..)) 65 | import Data.Thyme.LocalTime (Hour, Hours, Minute, Minutes) 66 | import GHC.Generics 67 | import GHC.Natural (Natural) 68 | 69 | #if MIN_VERSION_base(4,11,0) 70 | instance Semigroup Natural where 71 | a <> b = a + b 72 | #endif 73 | 74 | instance Monoid Natural where 75 | #if ! MIN_VERSION_base(4,11,0) 76 | a `mappend` b = a + b 77 | #endif 78 | mempty = 0 79 | 80 | -- | Org-mode document. 81 | data Document = Document 82 | { documentText :: Text -- ^ Text occurring before any Org headlines 83 | , documentHeadlines :: [Headline] -- ^ Toplevel Org headlines 84 | } deriving (Show, Eq, Generic, Typeable, Data) 85 | 86 | instance ToJSON Document where 87 | toEncoding = genericToEncoding defaultOptions 88 | 89 | instance FromJSON Document 90 | 91 | -- | Headline within an org-mode document. 92 | data Headline = Headline 93 | { depth :: Depth -- ^ Org headline nesting depth (1 is at the top), e.g: * or ** or *** 94 | , stateKeyword :: Maybe StateKeyword -- ^ State of the headline, e.g: TODO, DONE 95 | , priority :: Maybe Priority -- ^ Headline priority, e.g: [#A] 96 | , title :: Text -- ^ Primary text of the headline 97 | , timestamp :: Maybe Timestamp -- ^ A timestamp that may be embedded in the headline 98 | , stats :: Maybe Stats -- ^ Fraction of subtasks completed, e.g: [33%] or [1/2] 99 | , tags :: [Text] -- ^ Tags on the headline 100 | , section :: Section -- ^ The body underneath a headline 101 | , subHeadlines :: [Headline] -- ^ A list of sub-headlines 102 | } deriving (Show, Eq, Generic, Typeable, Data) 103 | 104 | instance ToJSON Headline where 105 | toEncoding = genericToEncoding defaultOptions 106 | 107 | instance FromJSON Headline 108 | 109 | -- | Headline nesting depth. 110 | newtype Depth = Depth Natural 111 | deriving (Eq, Show, Num, ToJSON, FromJSON, Generic, Typeable, Data) 112 | 113 | -- | Section of text directly following a headline. 114 | data Section = Section 115 | { sectionTimestamp :: Maybe Timestamp -- ^ A headline's section timestamp 116 | , sectionPlannings :: [Planning] -- ^ A list of planning records 117 | , sectionClocks :: [Clock] -- ^ A list of clocks 118 | , sectionProperties :: Properties -- ^ A map of properties from the :PROPERTY: drawer 119 | , sectionLogbook :: Logbook -- ^ A list of clocks from the :LOGBOOK: drawer 120 | , sectionContents :: [Content] -- ^ Content of Section 121 | } deriving (Show, Eq, Generic, Typeable, Data) 122 | 123 | instance ToJSON Section where 124 | toEncoding = genericToEncoding defaultOptions 125 | 126 | instance FromJSON Section 127 | 128 | sectionDrawer :: Section -> [Content] 129 | sectionDrawer s = filter isDrawer (sectionContents s) 130 | where 131 | isDrawer (Drawer _ _) = True 132 | isDrawer _ = False 133 | 134 | newtype Properties = Properties { unProperties :: InsOrdHashMap Text Text } 135 | deriving (Show, Eq, Semigroup, Monoid, ToJSON, FromJSON, Generic, Typeable, Data) 136 | 137 | data MarkupText 138 | = Plain Text 139 | | LaTeX Text 140 | | Verbatim Text 141 | | Code Text 142 | | Bold [MarkupText] 143 | | Italic [MarkupText] 144 | | UnderLine [MarkupText] 145 | | Strikethrough [MarkupText] 146 | | HyperLink 147 | { link :: Text 148 | , description :: Maybe Text 149 | } 150 | deriving (Show, Eq, Generic, Typeable, Data) 151 | 152 | instance ToJSON MarkupText where 153 | toEncoding = genericToEncoding defaultOptions 154 | 155 | instance FromJSON MarkupText 156 | 157 | newtype Item = Item [Content] 158 | deriving (Show, Eq, Semigroup, Monoid, ToJSON, FromJSON, Generic, Typeable, Data) 159 | 160 | data Content 161 | = 162 | OrderedList [Item] 163 | | UnorderedList [Item] 164 | | Paragraph [MarkupText] 165 | | Drawer 166 | { name :: Text 167 | , contents :: Text 168 | } deriving (Show, Eq, Generic, Typeable, Data) 169 | 170 | instance ToJSON Content where 171 | toEncoding = genericToEncoding defaultOptions 172 | 173 | instance FromJSON Content 174 | 175 | type Drawer = Content 176 | 177 | newtype Logbook = Logbook { unLogbook :: [Clock] } 178 | deriving (Show, Eq, Semigroup, Monoid, ToJSON, FromJSON, Generic, Typeable, Data) 179 | 180 | -- | Sum type indicating the active state of a timestamp. 181 | data ActiveState 182 | = Active 183 | | Inactive 184 | deriving (Show, Eq, Read, Generic, Typeable, Data) 185 | 186 | instance ToJSON ActiveState where 187 | toEncoding = genericToEncoding defaultOptions 188 | 189 | instance FromJSON ActiveState 190 | 191 | newtype Clock = Clock { unClock :: (Maybe Timestamp, Maybe Duration) } 192 | deriving (Show, Eq, ToJSON, FromJSON, Generic, Typeable, Data) 193 | 194 | -- | A generic data type for parsed org-mode time stamps, e.g: 195 | -- 196 | -- > <2015-03-27 Fri 10:20> 197 | -- > [2015-03-27 Fri 10:20 +4h] 198 | -- > <2015-03-27 Fri 10:20>--<2015-03-28 Sat 10:20> 199 | data Timestamp = Timestamp 200 | { tsTime :: DateTime -- ^ A datetime stamp 201 | , tsActive :: ActiveState -- ^ Active or inactive? 202 | , tsEndTime :: Maybe DateTime -- ^ A end-of-range datetime stamp 203 | } deriving (Show, Eq, Generic, Typeable, Data) 204 | 205 | instance ToJSON Timestamp where 206 | toEncoding = genericToEncoding defaultOptions 207 | 208 | instance FromJSON Timestamp 209 | 210 | instance ToJSON YearMonthDay where 211 | toJSON (YearMonthDay y m d) = 212 | object 213 | [ "year" .= y 214 | , "month" .= m 215 | , "day" .= d 216 | ] 217 | 218 | instance FromJSON YearMonthDay where 219 | parseJSON (Object v) = do 220 | y <- v .: "year" 221 | m <- v .: "month" 222 | d <- v .: "day" 223 | pure (YearMonthDay y m d) 224 | parseJSON _ = mzero 225 | 226 | type Weekday = Text 227 | type AbsTime = (Hours, Minutes) 228 | 229 | -- | A data type for parsed org-mode bracketed datetime stamps, e.g: 230 | -- 231 | -- > [2015-03-27 Fri 10:20 +4h] 232 | data BracketedDateTime = BracketedDateTime 233 | { datePart :: YearMonthDay 234 | , dayNamePart :: Maybe Weekday 235 | , timePart :: Maybe TimePart 236 | , repeat :: Maybe Repeater 237 | , delayPart :: Maybe Delay 238 | , activeState :: ActiveState 239 | } deriving (Show, Eq) 240 | 241 | -- | A sum type representing an absolute time part of a bracketed 242 | -- org-mode datetime stamp or a time range between two absolute 243 | -- timestamps. 244 | data TimePart 245 | = AbsoluteTime AbsTime 246 | | TimeStampRange (AbsTime, AbsTime) 247 | deriving (Eq, Ord, Show, Generic, Typeable, Data) 248 | 249 | instance ToJSON TimePart where 250 | toEncoding = genericToEncoding defaultOptions 251 | 252 | instance FromJSON TimePart 253 | 254 | -- | A data type for parsed org-mode datetime stamps. 255 | -- 256 | -- TODO: why do we have this data type and BracketedDateTime? They 257 | -- look almost exactly the same... 258 | data DateTime 259 | = DateTime 260 | { yearMonthDay :: YearMonthDay 261 | , dayName :: Maybe Text 262 | , hourMinute :: Maybe (Hour,Minute) 263 | , repeater :: Maybe Repeater 264 | , delay :: Maybe Delay 265 | } deriving (Show, Eq, Generic, Typeable, Data) 266 | 267 | instance ToJSON DateTime where 268 | toEncoding = genericToEncoding defaultOptions 269 | 270 | instance FromJSON DateTime 271 | 272 | -- | A sum type representing the repeater type of a repeater interval 273 | -- in a org-mode timestamp. 274 | data RepeaterType 275 | = RepeatCumulate 276 | | RepeatCatchUp 277 | | RepeatRestart 278 | deriving (Show, Eq, Generic, Typeable, Data) 279 | 280 | instance ToJSON RepeaterType where 281 | toEncoding = genericToEncoding defaultOptions 282 | 283 | instance FromJSON RepeaterType 284 | 285 | -- | A data type representing a repeater interval in a org-mode 286 | -- timestamp. 287 | data Repeater = Repeater 288 | { repeaterType :: RepeaterType -- ^ Type of repeater 289 | , repeaterValue :: Natural -- ^ Repeat value 290 | , repeaterUnit :: TimeUnit -- ^ Repeat time unit 291 | } deriving (Show, Eq, Generic, Typeable, Data) 292 | 293 | instance ToJSON Repeater where 294 | toEncoding = genericToEncoding defaultOptions 295 | 296 | instance FromJSON Repeater 297 | 298 | -- | A sum type representing the delay type of a delay value. 299 | data DelayType 300 | = DelayAll 301 | | DelayFirst 302 | deriving (Show, Eq, Generic, Typeable, Data) 303 | 304 | instance ToJSON DelayType where 305 | toEncoding = genericToEncoding defaultOptions 306 | 307 | instance FromJSON DelayType 308 | 309 | -- | A data type representing a delay value. 310 | data Delay = Delay 311 | { delayType :: DelayType -- ^ Type of delay 312 | , delayValue :: Natural -- ^ Delay value 313 | , delayUnit :: TimeUnit -- ^ Delay time unit 314 | } deriving (Show, Eq, Generic, Typeable, Data) 315 | 316 | instance ToJSON Delay where 317 | toEncoding = genericToEncoding defaultOptions 318 | 319 | instance FromJSON Delay 320 | 321 | -- | A sum type representing the time units of a delay. 322 | data TimeUnit 323 | = UnitYear 324 | | UnitWeek 325 | | UnitMonth 326 | | UnitDay 327 | | UnitHour 328 | deriving (Show, Eq, Generic, Typeable, Data) 329 | 330 | instance ToJSON TimeUnit where 331 | toEncoding = genericToEncoding defaultOptions 332 | 333 | instance FromJSON TimeUnit 334 | 335 | -- | A type representing a headline state keyword, e.g: @TODO@, 336 | -- @DONE@, @WAITING@, etc. 337 | newtype StateKeyword = StateKeyword { unStateKeyword :: Text } 338 | deriving (Show, Eq, Semigroup, Monoid, ToJSON, FromJSON, Generic, Typeable, Data) 339 | 340 | -- | A sum type representing the planning keywords. 341 | data PlanningKeyword = SCHEDULED | DEADLINE | CLOSED 342 | deriving (Show, Eq, Enum, Ord, Generic, Typeable, Data) 343 | 344 | instance ToJSON PlanningKeyword where 345 | toEncoding = genericToEncoding defaultOptions 346 | 347 | instance FromJSON PlanningKeyword 348 | 349 | -- | A type representing a map of planning timestamps. 350 | data Planning = Planning 351 | { keyword :: PlanningKeyword 352 | , timestamp :: Timestamp 353 | } deriving (Show, Eq, Generic, Typeable, Data) 354 | 355 | instance ToJSON Planning where 356 | toEncoding = genericToEncoding defaultOptions 357 | 358 | instance FromJSON Planning 359 | 360 | -- | A sum type representing the three default priorities: @A@, @B@, 361 | -- and @C@. 362 | data Priority = A | B | C 363 | deriving (Show, Read, Eq, Ord, Generic, Typeable, Data) 364 | 365 | instance ToJSON Priority where 366 | toEncoding = genericToEncoding defaultOptions 367 | 368 | instance FromJSON Priority 369 | 370 | -- | A data type representing a stats value in a headline, e.g @[2/3]@ 371 | -- in this headline: 372 | -- 373 | -- > * TODO [2/3] work on orgmode-parse 374 | data Stats 375 | = StatsPct Natural 376 | | StatsOf Natural Natural 377 | deriving (Show, Eq, Generic, Typeable, Data) 378 | 379 | instance ToJSON Stats where 380 | toEncoding = genericToEncoding defaultOptions 381 | 382 | instance FromJSON Stats 383 | 384 | type Duration = (Hour,Minute) 385 | -------------------------------------------------------------------------------- /test/Weekdays.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | module Weekdays where 3 | 4 | import qualified Data.Text as T 5 | 6 | weekdays :: [(String, [T.Text])] 7 | weekdays = [ 8 | ("aa_DJ.UTF-8", ["kam", "gum", "sab", "aca", "etl", "tal", "arb"]), 9 | ("aa_ER", ["Kam", "Gum", "Sab", "Aca", "Etl", "Tal", "Arb"]), 10 | ("aa_ER@saaho", ["Cam", "Jum", "Qun", "Nab", "San", "Sal", "Rab"]), 11 | ("aa_ET", ["Kam", "Gum", "Sab", "Aca", "Etl", "Tal", "Arb"]), 12 | ("af_ZA.UTF-8", ["Do", "Vr", "Sa", "So", "Ma", "Di", "Wo"]), 13 | ("am_ET", ["ሐሙስ", "ዓርብ", "ቅዳሜ", "እሑድ", "ሰኞ ", "ማክሰ", "ረቡዕ"]), 14 | ("an_ES.UTF-8", ["chu", "bie", "sab", "dom", "lun", "mar", "mie"]), 15 | ("ar_AE.UTF-8", ["خ", "ج", "س", "ح", "ن", "ث", "ر"]), 16 | ("ar_BH.UTF-8", ["خ", "ج", "س", "ح", "ن", "ث", "ر"]), 17 | ("ar_DZ.UTF-8", ["خ", "ج", "س", "ح", "ن", "ث", "ر"]), 18 | ("ar_EG.UTF-8", ["خ", "ج", "س", "ح", "ن", "ث", "ر"]), 19 | ("ar_IN", ["خ", "ج", "س", "ح", "ن", "ث", "ر"]), 20 | ("ar_IQ.UTF-8", ["خ", "ج", "س", "ح", "ن", "ث", "ر"]), 21 | ("ar_JO.UTF-8", ["الخميس", "الجمعة", "السبت", "الأحد", "الاثنين", "الثلاثاء", "الأربعاء"]), 22 | ("ar_KW.UTF-8", ["خ", "ج", "س", "ح", "ن", "ث", "ر"]), 23 | ("ar_LB.UTF-8", ["الخميس", "الجمعة", "السبت", "الأحد", "الاثنين", "الثلاثاء", "الأربعاء"]), 24 | ("ar_LY.UTF-8", ["خ", "ج", "س", "ح", "ن", "ث", "ر"]), 25 | ("ar_MA.UTF-8", ["خ", "ج", "س", "ح", "ن", "ث", "ر"]), 26 | ("ar_OM.UTF-8", ["خ", "ج", "س", "ح", "ن", "ث", "ر"]), 27 | ("ar_QA.UTF-8", ["خ", "ج", "س", "ح", "ن", "ث", "ر"]), 28 | ("ar_SA.UTF-8", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 29 | ("ar_SD.UTF-8", ["خ", "ج", "س", "ح", "ن", "ث", "ر"]), 30 | ("ar_SY.UTF-8", ["الخميس", "الجمعة", "السبت", "الأحد", "الاثنين", "الثلاثاء", "الأربعاء"]), 31 | ("ar_TN.UTF-8", ["خ", "ج", "س", "ح", "ن", "ث", "ر"]), 32 | ("ar_YE.UTF-8", ["خ", "ج", "س", "ح", "ن", "ث", "ر"]), 33 | ("az_AZ.UTF-8", ["cax", "cüm", "şnb", "baz", "ber", "çax", "çər"]), 34 | ("as_IN.UTF-8", ["বৃহষ্পতি", "শুক্ৰ", "শনি", "দেও", "সোম", "মঙ্গল", "বুধ"]), 35 | ("ast_ES.UTF-8", ["xue", "vie", "sáb", "dom", "llu", "mar", "mié"]), 36 | ("be_BY.UTF-8", ["Чцв", "Пят", "Суб", "Няд", "Пан", "Аўт", "Срд"]), 37 | ("be_BY@latin", ["Čać", "Pia", "Sub", "Nia", "Pan", "Aŭt", "Sie"]), 38 | ("bem_ZM", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 39 | ("ber_DZ", ["dör", "beş", "alt", "baz", "bir", "iki", "üçü"]), 40 | ("ber_MA", ["dör", "beş", "alt", "baz", "bir", "iki", "üçü"]), 41 | ("bg_BG.UTF-8", ["чт", "пт", "сб", "нд", "пн", "вт", "ср"]), 42 | ("bn_BD", ["বৃহঃ", "শুক্র", "শনি", "রবি", "সোম", "মঙ্গল", "বুধ"]), 43 | ("bn_IN", ["বৃহস্পতি", "শুক্র", "শনি", "রবি", "সোম", "মঙ্গল", "বুধ"]), 44 | ("bo_CN", ["པུར་", "སངས་", "སྤེན་", "ཉི་", "ཟླ་", "མིར་", "ལྷག་"]), 45 | ("bo_IN", ["པུར་", "སངས་", "སྤེན་", "ཉི་", "ཟླ་", "མིར་", "ལྷག་"]), 46 | ("br_FR.UTF-8", ["yao", "gwe", "sad", "sul", "lun", "meu", "mer"]), 47 | ("bs_BA.UTF-8", ["Čet", "Pet", "Sub", "Ned", "Pon", "Uto", "Sri"]), 48 | ("byn_ER", ["ኣምድ", "ኣርብ", "ሰ/ሽ", "ሰ/ቅ", "ሰኑ", "ሰሊጝ", "ለጓ"]), 49 | ("ca_AD.UTF-8", ["dj", "dv", "ds", "dg", "dl", "dt", "dc"]), 50 | ("ca_ES.UTF-8", ["dj", "dv", "ds", "dg", "dl", "dt", "dc"]), 51 | ("ca_ES.UTF-8@valencia", ["dj", "dv", "ds", "dg", "dl", "dt", "dc"]), 52 | ("ca_FR.UTF-8", ["dj", "dv", "ds", "dg", "dl", "dt", "dc"]), 53 | ("ca_IT.UTF-8", ["dj", "dv", "ds", "dg", "dl", "dt", "dc"]), 54 | ("crh_UA", ["Caq", "Cum", "Cer", "Baz", "Ber", "Sal", "Çar"]), 55 | ("cs_CZ.UTF-8", ["Čt", "Pá", "So", "Ne", "Po", "Út", "St"]), 56 | ("csb_PL", ["czw", "pią", "sob", "nie", "pòn", "wtó", "str"]), 57 | ("cv_RU", ["kş", "er", "šm", "vr", "tn", "yt", "jn"]), 58 | ("cy_GB.UTF-8", ["Iau", "Gwe", "Sad", "Sul", "Llu", "Maw", "Mer"]), 59 | ("da_DK.UTF-8", ["tor", "fre", "lør", "søn", "man", "tir", "ons"]), 60 | ("de_AT.UTF-8", ["Don", "Fre", "Sam", "Son", "Mon", "Die", "Mit"]), 61 | ("de_BE.UTF-8", ["Don", "Fre", "Sam", "Son", "Mon", "Die", "Mit"]), 62 | ("de_CH.UTF-8", ["Don", "Fre", "Sam", "Son", "Mon", "Die", "Mit"]), 63 | ("de_DE.UTF-8", ["Do", "Fr", "Sa", "So", "Mo", "Di", "Mi"]), 64 | ("de_LI.UTF-8", ["Don", "Fre", "Sam", "Son", "Mon", "Die", "Mit"]), 65 | ("de_LU.UTF-8", ["Don", "Fre", "Sam", "Son", "Mon", "Die", "Mit"]), 66 | ("dv_MV", ["ބުރާސްފަތި", "ހުކުރު", "ހޮނިހިރު", "އާދީއްތަ", "ހޯމަ", "އަންގާރަ", "ބުދަ"]), 67 | ("dz_BT", ["སངས་", "སྤེན་", "ཉི་", "ཟླ་", "མིར་", "ལྷག་", "པུར་"]), 68 | ("el_GR.UTF-8", ["Πεμ", "Παρ", "Σαβ", "Κυρ", "Δευ", "Τρι", "Τετ"]), 69 | ("el_CY.UTF-8", ["Πεμ", "Παρ", "Σαβ", "Κυρ", "Δευ", "Τρι", "Τετ"]), 70 | ("en_AG", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 71 | ("en_AU.UTF-8", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 72 | ("en_BW.UTF-8", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 73 | ("en_CA.UTF-8", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 74 | ("en_DK.UTF-8", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 75 | ("en_GB.UTF-8", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 76 | ("en_HK.UTF-8", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 77 | ("en_IE.UTF-8", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 78 | ("en_IN", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 79 | ("en_NG", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 80 | ("en_NZ.UTF-8", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 81 | ("en_PH.UTF-8", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 82 | ("en_SG.UTF-8", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 83 | ("en_US.UTF-8", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 84 | ("en_ZA.UTF-8", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 85 | ("en_ZM", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 86 | ("en_ZW.UTF-8", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 87 | ("eo.UTF-8", ["ĵaŭ", "ven", "sab", "dim", "lun", "mar", "mer"]), 88 | ("es_AR.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 89 | ("es_BO.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 90 | ("es_CL.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 91 | ("es_CO.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 92 | ("es_CR.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 93 | ("es_DO.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 94 | ("es_EC.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 95 | ("es_ES.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 96 | ("es_GT.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 97 | ("es_HN.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 98 | ("es_MX.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 99 | ("es_NI.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 100 | ("es_PA.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 101 | ("es_PE.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 102 | ("es_PR.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 103 | ("es_PY.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 104 | ("es_SV.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 105 | ("es_US.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 106 | ("es_UY.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 107 | ("es_VE.UTF-8", ["jue", "vie", "sáb", "dom", "lun", "mar", "mié"]), 108 | ("et_EE.UTF-8", ["N", "R", "L", "P", "E", "T", "K"]), 109 | ("eu_ES.UTF-8", ["og.", "or.", "lr.", "ig.", "al.", "ar.", "az."]), 110 | ("eu_FR.UTF-8", ["og.", "or.", "lr.", "ig.", "al.", "ar.", "az."]), 111 | -- TODO: lexical error in Haskell 112 | -- ("fa_IR", ["پنجشنبه", "جمعه", "شنبه", "یکشنبه", "دوشنبه", "سه‌شنبه", "چهارشنبه"]), 113 | ("ff_SN", ["naa", "mwd", "hbi", "dew", "aaɓ", "maw", "nje"]), 114 | ("fi_FI.UTF-8", ["to", "pe", "la", "su", "ma", "ti", "ke"]), 115 | ("fil_PH", ["Huw", "Biy", "Sab", "Lin", "Lun", "Mar", "Miy"]), 116 | ("fo_FO.UTF-8", ["hós", "frí", "ley", "sun", "mán", "týs", "mik"]), 117 | ("fr_BE.UTF-8", ["jeu", "ven", "sam", "dim", "lun", "mar", "mer"]), 118 | ("fr_CA.UTF-8", ["jeu", "ven", "sam", "dim", "lun", "mar", "mer"]), 119 | ("fr_CH.UTF-8", ["jeu", "ven", "sam", "dim", "lun", "mar", "mer"]), 120 | ("fr_FR.UTF-8", ["jeu.", "ven.", "sam.", "dim.", "lun.", "mar.", "mer."]), 121 | ("fr_LU.UTF-8", ["jeu", "ven", "sam", "dim", "lun", "mar", "mer"]), 122 | ("fur_IT", ["Joi", "Vin", "Sab", "Dom", "Lun", "Mar", "Mia"]), 123 | ("fy_NL", ["To", "Fr", "Sn", "Sn", "Mo", "Ti", "Wo"]), 124 | ("fy_DE", ["Ddg", "Fdg", "Swd", "Sdg", "Mdg", "Dsg", "Mwk"]), 125 | ("ga_IE.UTF-8", ["Déar", "Aoine", "Sath", "Domh", "Luan", "Máirt", "Céad"]), 126 | ("gd_GB.UTF-8", ["Diar", "Diha", "Disa", "Dido", "Dilu", "Dim", "Dic"]), 127 | ("gez_ER", ["ሐሙስ", "ዓርበ", "ቀዳሚ", "እኁድ", "ሰኑይ", "ሠሉስ", "ራብዕ"]), 128 | ("gez_ER@abegede", ["ሐሙስ", "ዓርበ", "ቀዳሚ", "እኁድ", "ሰኑይ", "ሠሉስ", "ራብዕ"]), 129 | ("gez_ET", ["ሐሙስ", "ዓርበ", "ቀዳሚ", "እኁድ", "ሰኑይ", "ሠሉስ", "ራብዕ"]), 130 | ("gez_ET@abegede", ["ሐሙስ", "ዓርበ", "ቀዳሚ", "እኁድ", "ሰኑይ", "ሠሉስ", "ራብዕ"]), 131 | ("gl_ES.UTF-8", ["Xov", "Ven", "Sáb", "Dom", "Lun", "Mar", "Mér"]), 132 | ("gu_IN", ["ગુરુ", "શુક્ર", "શનિ", "રવિ", "સોમ", "મંગળ", "બુધ"]), 133 | ("gv_GB.UTF-8", ["Jerd", "Jeh", "Jes", "Jed", "Jel", "Jem", "Jerc"]), 134 | ("ha_NG", ["Alh", "Jum", "Asa", "Lah", "Lit", "Tal", "Lar"]), 135 | ("he_IL.UTF-8", ["ה'", "ו'", "ש'", "א'", "ב'", "ג'", "ד'"]), 136 | ("hi_IN", ["गुरु ", "शुक्र ", "शनि ", "रवि ", "सोम ", "मंगल ", "बुध "]), 137 | ("hne_IN", ["बिर ", "सुक", "सनि", "इत ", "सोम ", "मंग ", "बुध "]), 138 | ("hr_HR.UTF-8", ["Čet", "Pet", "Sub", "Ned", "Pon", "Uto", "Sri"]), 139 | ("hsb_DE.UTF-8", ["Št", "Pj", "So", "Nj", "Pó", "Wu", "Sr"]), 140 | ("ht_HT", ["jEd.", "van.", "sam.", "dim.", "lEn.", "mad.", "mèk."]), 141 | ("hu_HU.UTF-8", ["cs", "p", "szo", "v", "h", "k", "sze"]), 142 | ("hy_AM", ["Հնգ", "Ուր", "Շբթ", "Կրկ", "Երկ", "Երք", "Չրք"]), 143 | ("ia", ["jov", "ven", "sab", "dom", "lun", "mar", "mer"]), 144 | ("id_ID.UTF-8", ["Kam", "Jum", "Sab", "Min", "Sen", "Sel", "Rab"]), 145 | ("ig_NG", ["tọs", "fra", "sat", "sọn", "mọn", "tuz", "wen"]), 146 | ("ik_CA", ["Sis", "Tal", "Maq", "Min", "Sav", "Ila", "Qit"]), 147 | ("is_IS.UTF-8", ["fim", "fös", "lau", "sun", "mán", "þri", "mið"]), 148 | ("it_CH.UTF-8", ["gio", "ven", "sab", "dom", "lun", "mar", "mer"]), 149 | ("it_IT.UTF-8", ["gio", "ven", "sab", "dom", "lun", "mar", "mer"]), 150 | ("iu_CA", ["ᕿ", "ᐅ", "ᓯ", "ᓈ", "ᓇ", "ᓕ", "ᐱ"]), 151 | ("iw_IL.UTF-8", ["ה'", "ו'", "ש'", "א'", "ב'", "ג'", "ד'"]), 152 | ("ja_JP.UTF-8", ["木", "金", "土", "日", "月", "火", "水"]), 153 | ("ka_GE.UTF-8", ["ხუთ", "პარ", "შაბ", "კვი", "ორშ", "სამ", "ოთხ"]), 154 | ("kk_KZ.UTF-8", ["Бс", "Жм", "Сн", "Жк", "Дс", "Сс", "Ср"]), 155 | ("kl_GL.UTF-8", ["sis", "tal", "arf", "sab", "ata", "mar", "pin"]), 156 | ("km_KH", ["ព្រ", "សុ", "ស", "អា", "ច", "អ", "ពុ"]), 157 | ("kn_IN", ["ಗು", "ಶು", "ಶ", "ರ", "ಸೋ", "ಮಂ", "ಬು"]), 158 | ("ko_KR.UTF-8", ["목", "금", "토", "일", "월", "화", "수"]), 159 | ("kok_IN", ["बेरेसतार", "शुकरार", "शेनवार", "आयतार", "सोमार", "मंगळवार", "बुधवार"]), 160 | ("ks_IN", ["برىسوار", "جمع", "بٹوار", "آتهوار", "ژءنتروار", "بوءںوار", "بودهوار"]), 161 | ("ks_IN@devanagari", ["ब्रस््", "जुम", "बट", "आथ्", "चन्दर्", "बोम्", "बोघ"]), 162 | ("ku_TR.UTF-8", ["Pş", "În", "Ş", "Yş", "Dş", "Sş", "Çş"]), 163 | ("kw_GB.UTF-8", ["Yow", "Gwe", "Sad", "Sul", "Lun", "Mth", "Mhr"]), 164 | ("ky_KG", ["бш", "жм", "иш", "жк", "дш", "ше", "ша"]), 165 | ("lg_UG.UTF-8", ["Lw4", "Lw5", "Lw6", "Sab", "Bal", "Lw2", "Lw3"]), 166 | -- TODO: lexical error in Haskell 167 | -- ("li_BE", ["dón", "vri", "z‘o", "zón", "mao", "dae", "goo"]), 168 | -- ("li_NL", ["dón", "vri", "z‘o", "zón", "mao", "dae", "goo"]), 169 | ("lo_LA", ["ພຫ.", "ສ.", "ສ.", "ອາ.", "ຈ.", "ຄ.", "ພ."]), 170 | ("lt_LT.UTF-8", ["Kt", "Pn", "Št", "Sk", "Pr", "An", "Tr"]), 171 | ("lv_LV.UTF-8", ["C ", "Pk", "S ", "Sv", "P ", "O ", "T "]), 172 | ("mai_IN", ["गुरु ", "शुक्र ", "शनि ", "रवि ", "सोम ", "मंगल ", "बुध "]), 173 | ("mg_MG.UTF-8", ["lkm", "zom", "sab", "lhd", "lts", "tlt", "lrb"]), 174 | ("mi_NZ.UTF-8", ["Tāi", "Pa", "Hā", "Ta", "Ma", "Tū", "We"]), 175 | ("mk_MK.UTF-8", ["чет", "пет", "саб", "нед", "пон", "вто", "сре"]), 176 | ("ml_IN", ["വ്യാ", "വെ", "ശ", "ഞാ", "തി", "ചൊ", "ബു"]), 177 | ("mn_MN", ["Пү", "Ба", "Бя", "Ня", "Да", "Мя", "Лх"]), 178 | ("mr_IN", ["गुरु", "शुक्र", "शनि", "रवि", "सोम", "मंगळ", "बुध"]), 179 | ("ms_MY.UTF-8", ["Kha", "Jum", "Sab", "Ahd", "Isn", "Sel", "Rab"]), 180 | ("mt_MT.UTF-8", ["Ħam", "Ġim", "Sib", "Ħad", "Tne", "Tli", "Erb"]), 181 | ("my_MM", ["တေး", "သော", "နေ", "နွေ", "လာ", "ဂါ", "ဟူး"]), 182 | ("nan_TW@latin", ["p4", "p5", "p6", "lp", "p1", "p2", "p3"]), 183 | ("nb_NO.UTF-8", ["to.", "fr.", "lø.", "sø.", "ma.", "ti.", "on."]), 184 | ("nds_DE", ["Dunn", "Free", "Svd.", "Sdag", "Maan", "Ding", "Migg"]), 185 | ("nds_NL", ["Ddg", "Fdg", "Swd", "Sdg", "Mdg", "Dsg", "Mwk"]), 186 | ("ne_NP", ["बिहि ", "शुक्र ", "शनि ", "आइत ", "सोम ", "मंगल ", "बुध "]), 187 | ("nl_AW", ["do", "vr", "za", "zo", "ma", "di", "wo"]), 188 | ("nl_BE.UTF-8", ["do", "vr", "za", "zo", "ma", "di", "wo"]), 189 | ("nl_NL.UTF-8", ["do", "vr", "za", "zo", "ma", "di", "wo"]), 190 | ("nn_NO.UTF-8", ["to.", "fr.", "la.", "su.", "må.", "ty.", "on."]), 191 | ("nr_ZA", ["Ne", "Hla", "Gqi", "Son", "Mvu", "Bil", "Tha"]), 192 | ("nso_ZA", ["Ne", "Hla", "Mok", "Son", "Moš", "Bed", "Rar"]), 193 | ("oc_FR.UTF-8", ["jòu", "ven", "sab", "dim", "lun", "mar", "mec"]), 194 | ("om_ET", ["Kam", "Jim", "San", "Dil", "Wix", "Qib", "Rob"]), 195 | ("om_KE.UTF-8", ["Kam", "Jim", "San", "Dil", "Wix", "Qib", "Rob"]), 196 | ("or_IN", ["ଗୁରୁ", "ଶୁକ୍ର", "ଶନି", "ରବି", "ସୋମ", "ମଙ୍ଗଳ", "ବୁଧ"]), 197 | ("os_RU", ["Цпр", "Мрб", "Сбт", "Хцб", "Крс", "Дцг", "Æрт"]), 198 | ("pa_IN", ["ਵੀਰ ", "ਸ਼ੁੱਕਰ ", "ਸ਼ਨਿੱਚਰ ", "ਐਤ ", "ਸੋਮ ", "ਮੰਗਲ ", "ਬੁੱਧ "]), 199 | ("pa_PK", ["جمعرات", "جمعه", "هفته", "اتوار", "پير", "منگل", "بدھ"]), 200 | ("pap_AN", ["ra", "bi", "sa", "do", "lu", "ma", "we"]), 201 | ("pl_PL.UTF-8", ["czw", "pią", "sob", "nie", "pon", "wto", "śro"]), 202 | ("ps_AF", ["پ.", "ج.", "ش.", "ی.", "د.", "س.", "چ."]), 203 | ("pt_BR.UTF-8", ["Qui", "Sex", "Sáb", "Dom", "Seg", "Ter", "Qua"]), 204 | ("pt_PT.UTF-8", ["Qui", "Sex", "Sáb", "Dom", "Seg", "Ter", "Qua"]), 205 | ("ro_RO.UTF-8", ["Jo", "Vi", "Sb", "Du", "Lu", "Ma", "Mi"]), 206 | ("ru_RU.UTF-8", ["Чтв", "Птн", "Сбт", "Вск", "Пнд", "Втр", "Срд"]), 207 | ("ru_UA.UTF-8", ["Чтв", "Птн", "Суб", "Вск", "Пнд", "Вто", "Срд"]), 208 | ("rw_RW", ["Kan", "Gnu", "Gnd", "Mwe", "Mbe", "Kab", "Gtu"]), 209 | ("sa_IN", ["बृहस्पतिः", "शुक्र", "शनि:", "रविः", "सोम:", "मंगल:", "बुध:"]), 210 | ("sc_IT", ["Jòb", "Cen", "Sàb", "Dom", "Lun", "Mar", "Mèr"]), 211 | ("sd_IN", ["وسپت", "جُمو", "ڇنڇر", "آرتوارُ", "سومرُ", "منگلُ", "ٻُڌرُ"]), 212 | ("sd_IN@devanagari", ["विस्पति", "जुमो", "छंछस", "आर्तवारू", "सूमरू", "मंगलू", "ॿुधरू"]), 213 | ("se_NO", ["duor", "bear", "láv", "sotn", "vuos", "maŋ", "gask"]), 214 | ("shs_CA", ["Sme", "Sts", "Stq", "Sxe", "Spe", "Sel", "Ske"]), 215 | -- TODO: lexical error in Haskell 216 | -- ("si_LK", ["බ්‍ර", "සි", "සෙ", "ඉ", "ස", "අ", "බ"]), 217 | ("sid_ET", ["Ham", "Arb", "Qid", "Sam", "San", "Mak", "Row"]), 218 | ("sk_SK.UTF-8", ["Št", "Pi", "So", "Ne", "Po", "Ut", "St"]), 219 | ("sl_SI.UTF-8", ["čet", "pet", "sob", "ned", "pon", "tor", "sre"]), 220 | ("so_DJ.UTF-8", ["kha", "jim", "sab", "axa", "isn", "sal", "arb"]), 221 | ("so_ET", ["Kha", "Jim", "Sab", "Axa", "Isn", "Sal", "Arb"]), 222 | ("so_KE.UTF-8", ["Kha", "Jim", "Sab", "Axa", "Isn", "Sal", "Arb"]), 223 | ("so_SO.UTF-8", ["Kha", "Jim", "Sab", "Axa", "Isn", "Sal", "Arb"]), 224 | ("sq_AL.UTF-8", ["Enj ", "Pre ", "Sht ", "Die ", "Hën ", "Mar ", "Mër "]), 225 | ("sq_MK", ["Enj ", "Pre ", "Sht ", "Die ", "Hën ", "Mar ", "Mër "]), 226 | ("sr_ME", ["чет", "пет", "суб", "нед", "пон", "уто", "сри"]), 227 | ("sr_RS", ["чет", "пет", "суб", "нед", "пон", "уто", "сре"]), 228 | ("sr_RS@latin", ["čet", "pet", "sub", "ned", "pon", "uto", "sre"]), 229 | ("ss_ZA", ["Ne", "Hla", "Mgc", "Son", "Mso", "Bil", "Tsa"]), 230 | ("st_ZA.UTF-8", ["Ne", "Hla", "Moq", "Son", "Mma", "Bed", "Rar"]), 231 | ("sv_FI.UTF-8", ["tor", "fre", "lör", "sön", "mån", "tis", "ons"]), 232 | ("sv_SE.UTF-8", ["tor", "fre", "lör", "sön", "mån", "tis", "ons"]), 233 | ("sw_KE", ["Alh", "Ij", "J1", "J2", "J3", "J4", "J5"]), 234 | ("sw_TZ", ["Alh", "Ij", "J1", "J2", "J3", "J4", "J5"]), 235 | ("ta_IN", ["வி", "வெ", "ச", "ஞா", "தி", "செ", "பு"]), 236 | ("te_IN", ["గురు", "శుక్ర", "శని", "ఆది", "సోమ", "మంగళ", "బుధ"]), 237 | ("tg_TJ.UTF-8", ["Чтв", "Птн", "Сбт", "Вск", "Пнд", "Втр", "Срд"]), 238 | ("th_TH.UTF-8", ["พฤ.", "ศ.", "ส.", "อา.", "จ.", "อ.", "พ."]), 239 | ("ti_ER", ["ሓሙስ", "ዓርቢ", "ቀዳም", "ሰንበ", "ሰኑይ", "ሰሉስ", "ረቡዕ"]), 240 | ("ti_ET", ["ሓሙስ", "ዓርቢ", "ቀዳም", "ሰንበ", "ሰኑይ", "ሰሉስ", "ረቡዕ"]), 241 | ("tig_ER", ["ከሚሽ", "ጅምዓ", "ሰ/ን", "ሰ/ዓ", "ሰኖ ", "ታላሸ", "ኣረር"]), 242 | ("tk_TM", ["Sog", "Ann", "Ruh", "Dyn", "Baş", "Yaş", "Hoş"]), 243 | ("tl_PH.UTF-8", ["Huw", "Biy", "Sab", "Lin", "Lun", "Mar", "Miy"]), 244 | ("tn_ZA", ["Ne", "Tlh", "Mat", "Tsh", "Mos", "Bed", "Rar"]), 245 | ("tr_CY.UTF-8", ["Prş", "Cum", "Cts", "Paz", "Pzt", "Sal", "Çrş"]), 246 | ("tr_TR.UTF-8", ["Prş", "Cum", "Cts", "Paz", "Pzt", "Sal", "Çrş"]), 247 | ("ts_ZA", ["Ne", "Tlh", "Mug", "Son", "Mus", "Bir", "Har"]), 248 | ("tt_RU.UTF-8", ["Пәнҗ", "Җом", "Шим", "Якш", "Дыш", "Сиш", "Чәрш"]), 249 | ("tt_RU.UTF-8@iqtelif", ["Pen", "Com", "Şim", "Yek", "Düş", "Siş", "Çer"]), 250 | ("ug_CN", ["Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"]), 251 | ("uk_UA.UTF-8", ["чт", "пт", "сб", "нд", "пн", "вт", "ср"]), 252 | ("ur_PK", ["جمعرات", "جمعه", "هفته", "اتوار", "پير", "منگل", "بدھ"]), 253 | ("uz_UZ.UTF-8", ["Pay", "Ju", "Sha", "Yak", "Du", "Se", "Cho"]), 254 | ("uz_UZ@cyrillic", ["Пай", "Жум", "Шан", "Якш", "Душ", "Сеш", "Чор"]), 255 | ("ve_ZA", ["ṋa", "Ṱan", "Mug", "Swo", "Mus", "Vhi", "Rar"]), 256 | ("vi_VN", ["T5", "T6", "T7", "CN", "T2", "T3", "T4"]), 257 | ("wa_BE.UTF-8", ["dju", "vén", "sem", "dim", "lon", "mår", "mie"]), 258 | ("wo_SN", ["alx", "ajj", "gaa", "dib", "alt", "tal", "all"]), 259 | ("xh_ZA.UTF-8", ["Sin", "Hla", "Mgq", "Caw", "Mvu", "Bin", "Tha"]), 260 | ("yi_US.UTF-8", ["דאָנ'", "פֿרײַ'", "שבת", "זונ'", "מאָנ'", "דינ'", "מיט'"]), 261 | ("yo_NG", ["THU", "FRI", "SAT", "SUN", "MON", "TUE", "WED"]), 262 | ("zh_CN.UTF-8", ["四", "五", "六", "日", "一", "二", "三"]), 263 | ("zh_HK.UTF-8", ["四", "五", "六", "日", "一", "二", "三"]), 264 | ("zh_SG.UTF-8", ["星期四", "星期五", "星期六", "星期日", "星期一", "星期二", "星期三"]), 265 | ("zh_TW.UTF-8", ["四", "五", "六", "日", "一", "二", "三"]), 266 | ("zu_ZA.UTF-8", ["Sin", "Hla", "Mgq", "Son", "Mso", "Bil", "Tha"]) 267 | ] 268 | --------------------------------------------------------------------------------