diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2af1fc --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.stack-work/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c85714a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: haskell + +sudo: false + +cache: + directories: + - $HOME/.stack + +before_install: + - mkdir -p ~/.local/bin + - export PATH=~/.local/bin:$PATH + - travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' + - chmod a+x ~/.local/bin/stack + +install: + - stack install cabal-install --install-ghc + +script: + - stack setup + - stack build + - stack test + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..98d9811 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Iky + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/ogmarkup.cabal b/ogmarkup.cabal new file mode 100644 index 0000000..1427a28 --- /dev/null +++ b/ogmarkup.cabal @@ -0,0 +1,34 @@ +name: ogmarkup +version: 0.1.0.0 +synopsis: Language Markup Parser for Ogma +description: Please see README.md +homepage: http://gitlab.com/ogma/ogmarkup +license: MIT +license-file: LICENSE +author: Iky +maintainer: hi@ikyushii.eu +copyright: 2016 Iky +category: Web +build-type: Simple +-- extra-source-files: +cabal-version: >=1.10 + +library + hs-source-dirs: src + exposed-modules: Ogmarkup.Parser, + Ogmarkup.Ast + build-depends: base >= 4.7 && < 5, parsec == 3.1.9 + default-language: Haskell2010 + +test-suite ogmadown-test + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Spec.hs + build-depends: base, hspec, + ogmarkup, parsec + ghc-options: -threaded -rtsopts -with-rtsopts=-N + default-language: Haskell2010 + +source-repository head + type: git + location: https://gitlab.com/ogma/ogmarkup diff --git a/src/Ogmarkup/Ast.hs b/src/Ogmarkup/Ast.hs new file mode 100644 index 0000000..f587a61 --- /dev/null +++ b/src/Ogmarkup/Ast.hs @@ -0,0 +1,66 @@ +module Ogmarkup.Ast ( + OgmaText ( + Word, + Semicolon, + Colon, + OpenQuote, + CloseQuote, + QuestionMark, + ExclamationMark, + LongDash, + Comma, + Point, + SuspensionPoints + ), + OgmaFormat ( + Raw, + WeakEmph, + StrongEmph + ), + OgmaDialog (Simple, + WithSay + ), + OgmaComponent (Teller, + Audible, + Thought + ), + OgmaParagraph (Story, + Quote + ) + ) where + +data OgmaText = + Word String + | Semicolon + | Colon + | OpenQuote + | CloseQuote + | QuestionMark + | ExclamationMark + | LongDash + | Comma + | Point + | SuspensionPoints + deriving (Show,Eq) + +data OgmaFormat = + Raw [OgmaText] + | WeakEmph [OgmaText] + | StrongEmph [OgmaText] + deriving (Show,Eq) + +data OgmaDialog = + Simple [OgmaFormat] String + | WithSay [OgmaFormat] [OgmaFormat] [OgmaFormat] String + deriving (Show,Eq) + +data OgmaComponent = + Teller [OgmaFormat] + | Audible OgmaDialog -- String + | Thought OgmaDialog -- String + deriving (Show,Eq) + +data OgmaParagraph = + Story [OgmaComponent] + | Quote [[OgmaComponent]] OgmaFormat + deriving (Show,Eq) diff --git a/src/Ogmarkup/Parser.hs b/src/Ogmarkup/Parser.hs new file mode 100644 index 0000000..bc33842 --- /dev/null +++ b/src/Ogmarkup/Parser.hs @@ -0,0 +1,137 @@ +module Ogmarkup.Parser ( + specialchar, + word, + text, + raw, + strongemph, + weakemph, + paragraph, + format, + dialog, + teller, + component, + ) where + +import Text.ParserCombinators.Parsec +import Ogmarkup.Ast + +textmarker :: GenParser Char st () +textmarker = oneOf "«»—?!;:.," >> return () + +formatmarker :: GenParser Char st () +formatmarker = oneOf "*|" >> return () + +opendialogmarker :: GenParser Char st () +opendialogmarker = oneOf "[<" >> return () + +closedialogmarker :: GenParser Char st () +closedialogmarker = oneOf ">]" >> return () + +dialogmarker :: GenParser Char st () +dialogmarker = opendialogmarker <|> closedialogmarker + +marker :: GenParser Char st () +marker = (space >> return ()) <|> textmarker <|> formatmarker <|> dialogmarker <|> eof + +ogmaLookAhead parser = (lookAhead $ try parser) >> return () + +manyTillEof parser till = manyTill parser (ogmaLookAhead till <|> eof) + +many1TillEof parser till = do + notFollowedBy till + ps <- manyTill parser (ogmaLookAhead till <|> eof) + + return ps + +paragraph :: GenParser Char st OgmaParagraph +paragraph = manyTill component eof >>= return . Story + +component :: GenParser Char st OgmaComponent +component = spaces *> (try audible <|> try thought <|> teller) <* spaces + +audible :: GenParser Char st OgmaComponent +audible = dialog '[' ']' >>= return . Audible + +thought :: GenParser Char st OgmaComponent +thought = dialog '<' '>' >>= return . Thought + +teller :: GenParser Char st OgmaComponent +teller = many1TillEof format opendialogmarker >>= return . Teller + +dialog :: Char -> Char -> GenParser Char st OgmaDialog +dialog c e = do + char c + d <- many1TillEof format (char e <|> char '|') + try (do + char '|' + i <- many1TillEof format (char '|') + char '|' + spaces + d' <- manyTillEof format (char e) + char e + char '(' + a <- many letter + char ')' + + return $ WithSay d i d' a) <|> (do + char e + char '(' + a <- many letter + char ')' + return $ Simple d a) + +format :: GenParser Char st OgmaFormat +format = spaces *> (try strongemph <|> try weakemph <|> raw) <* spaces + +weakemph :: GenParser Char st OgmaFormat +weakemph = do + char '*' + t <- many1TillEof text (char '*') + char '*' + + return $ WeakEmph t + +strongemph :: GenParser Char st OgmaFormat +strongemph = do + string "**" + t <- many1TillEof text (string "**") + string "**" + + return $ StrongEmph t + +raw :: GenParser Char st OgmaFormat +raw = (many1TillEof text (formatmarker <|> dialogmarker)) >>= return . Raw + +text :: GenParser Char st OgmaText +text = spaces *> (specialchar <|> (notFollowedBy marker >> word)) <* spaces + +word :: GenParser Char st OgmaText +word = do + w <- many1TillEof anyChar marker + return $ Word w + +specialchar :: GenParser Char st OgmaText +specialchar = colon + <|> semicolon + <|> longDash + <|> openQuote + <|> closeQuote + <|> exclamationMark + <|> questionMark + <|> comma + <|> try suspension + <|> point + +parseSpecial :: String -> OgmaText -> GenParser Char st OgmaText +parseSpecial s r = string s >> return r + +questionMark = parseSpecial "?" QuestionMark +exclamationMark = parseSpecial "!" ExclamationMark +openQuote = parseSpecial "«" OpenQuote +closeQuote = parseSpecial "»" CloseQuote +longDash = parseSpecial "—" LongDash +semicolon = parseSpecial ";" Semicolon +colon = parseSpecial ":" Colon +comma = parseSpecial "," Comma +point = parseSpecial "." Point +suspension = string ".." >> many (char '.') >> return SuspensionPoints diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..657a1c2 --- /dev/null +++ b/stack.yaml @@ -0,0 +1,35 @@ +# For more information, see: http://docs.haskellstack.org/en/stable/yaml_configuration.html + +# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2) +resolver: lts-5.4 + +# Local packages, usually specified by relative directory name +packages: +- '.' + +# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3) +extra-deps: [] + +# Override default flag values for local packages and extra-deps +flags: {} + +# Extra package databases containing global packages +extra-package-dbs: [] + +# Control whether we use the GHC we find on the path +# system-ghc: true + +# Require a specific version of stack, using version ranges +# require-stack-version: -any # Default +# require-stack-version: >= 1.0.0 + +# Override the architecture used by stack, especially useful on Windows +# arch: i386 +# arch: x86_64 + +# Extra directories used by stack for building +# extra-include-dirs: [/path/to/dir] +# extra-lib-dirs: [/path/to/dir] + +# Allow a newer minor version of GHC than the snapshot specifies +# compiler-check: newer-minor diff --git a/test/Parser.hs b/test/Parser.hs new file mode 100644 index 0000000..7e05744 --- /dev/null +++ b/test/Parser.hs @@ -0,0 +1,285 @@ +module Parser ( + specialcharSpec, + wordSpec, + textSpec, + rawSpec, + strongemphSpec, + weakemphSpec, + formatSpec, + dialogSpec, + tellerSpec, + componentSpec, + paragraphSpec, + ) where + +import Test.Hspec +import Text.ParserCombinators.Parsec +import Data.Either + +import Ogmarkup.Ast +import Ogmarkup.Parser + +specialcharSpec :: Spec +specialcharSpec = do + describe "specialchar" $ do + it "should return the relevant character" $ do + (parse specialchar "(Spec)" "!") `shouldBe` (Right ExclamationMark) + + it "should return a suspension points rather than several points" $ do + (parse specialchar "(Spec)" "....") `shouldBe` (Right SuspensionPoints) + + it "should fail if the output is not a special character" $ do + (parse specialchar "(Spec)" "x") `shouldSatisfy` isLeft + +wordSpec :: Spec +wordSpec = do + describe "word" $ do + it "should return the relevant word" $ do + (parse word "(Spec)" "bonjour") `shouldBe` (Right $ Word "bonjour") + it "should fail if the output is a special character" $ do + (parse specialchar "(Spec)" "*") `shouldSatisfy` isLeft + +textSpec :: Spec +textSpec = do + describe "text" $ do + it "should return a word if the output is a word" $ do + (parse text "(Spec)" "bonjour") `shouldBe` (Right $ Word "bonjour") + it "should return a special character if the output is a special character" $ do + (parse text "(Spec)" "?") `shouldBe` (Right $ QuestionMark) + it "should consume white spaces" $ do + (parse text "(Spec)" " bonjour ") `shouldBe` (Right $ Word "bonjour") + +rawSpec :: Spec +rawSpec = do + describe "raw" $ do + it "should return a list of text" $ do + (parse raw "(Spec)" "bonjour madame!") `shouldBe` (Right $ Raw [Word "bonjour", + Word "madame", + ExclamationMark]) + it "should stop whith a format marker" $ do + (parse raw "(Spec)" "bonjour madame*!") `shouldBe` (Right $ Raw [Word "bonjour", + Word "madame"]) + + it "should stop whith a dialog marker" $ do + (parse raw "(Spec)" "bonjour madame|!") `shouldBe` (Right $ Raw [Word "bonjour", + Word "madame"]) + + it "should consume inner whitespaces" $ do + (parse raw "(Spec)" "bonjour madame !") + `shouldBe` (Right $ Raw [Word "bonjour", Word "madame", ExclamationMark]) + +strongemphSpec :: Spec +strongemphSpec = do + describe "strongemph" $ do + it "should return a list of text" $ do + (parse strongemph "(Spec)" "**bonjour madame!**") + `shouldBe` (Right $ StrongEmph [Word "bonjour", Word "madame", ExclamationMark]) + + it "should consume inner white spaces" $ do + (parse strongemph "(Spec)" "** bonjour madame! **") + `shouldBe` (Right $ StrongEmph [Word "bonjour", Word "madame", ExclamationMark]) + + it "should fail if the end marker is missing before the end" $ do + (parse strongemph "(Spec)" "**bonjour madame!") + `shouldSatisfy` isLeft + + it "... or before another marker" $ do + (parse strongemph "(Spec)" "**bonjour madame!|") + `shouldSatisfy` isLeft + + it "... or incomplete" $ do + (parse strongemph "(Spec)" "**bonjour madame!*") + `shouldSatisfy` isLeft + +weakemphSpec :: Spec +weakemphSpec = do + describe "weakemph" $ do + it "should return a list of text" $ do + (parse weakemph "(Spec)" "*bonjour madame!*") + `shouldBe` (Right $ WeakEmph [Word "bonjour", Word "madame", ExclamationMark]) + + it "should consume inner white spaces" $ do + (parse weakemph "(Spec)" "* bonjour madame! *") + `shouldBe` (Right $ WeakEmph [Word "bonjour", Word "madame", ExclamationMark]) + + it "should fail if the end marker is missing before the end" $ do + (parse weakemph "(Spec)" "*bonjour madame!") + `shouldSatisfy` isLeft + + it "... or before another marker" $ do + (parse weakemph "(Spec)" "*bonjour madame!|") + `shouldSatisfy` isLeft + +formatSpec :: Spec +formatSpec = do + describe "format" $ do + it "should return a strong emphasis" $ do + (parse format "(Spec)" "**bonjour madame?**") + `shouldBe` (Right $ StrongEmph [Word "bonjour", Word "madame", QuestionMark]) + + it "should return a weak emphasis" $ do + (parse format "(Spec)" "*bonjour madame?*") + `shouldBe` (Right $ WeakEmph [Word "bonjour", Word "madame", QuestionMark]) + + it "should return a raw text" $ do + (parse format "(Spec)" "bonjour madame?") + `shouldBe` (Right $ Raw [Word "bonjour", Word "madame", QuestionMark]) + + it "should stop with a marker" $ do + (parse format "(Spec)" "bonjour] madame?") + `shouldBe` (Right $ Raw [Word "bonjour"]) + + it "should consume outter whitespaces (1/2)" $ do + (parse format "(Spec)" " **bonjour** ") + `shouldBe` (Right $ StrongEmph [Word "bonjour"]) + + it "should consume outter whitespaces (2/2)" $ do + (parse format "(Spec)" " bonjour ") + `shouldBe` (Right $ Raw [Word "bonjour"]) + + it "should failed when no closing format marker is found (1/2)" $ do + (parse format "(Spec)" "**bonjour madame?") + `shouldSatisfy` isLeft + + it "should failed when no closing format marker is found (2/2)" $ do + (parse format "(Spec)" "**bonjour madame?<") + `shouldSatisfy` isLeft + +dialogSpec :: Spec +dialogSpec = do + describe "dialog" $ do + it "should parse a simple dialog" $ do + (parse (dialog '[' ']') "(Spec)" "[bonjour](tata)") + `shouldBe` (Right $ Simple [Raw $ [Word "bonjour"]] "tata") + + it "should parse a dialog 'with say'" $ do + (parse (dialog '[' ']') "(Spec)" "[bonjour |dit-il| comment](toto)") + `shouldBe` (Right $ WithSay [Raw [Word "bonjour"]] + [Raw [Word "dit-il"]] + [Raw [Word "comment"]] + "toto") + + it "should deal with multiple formats" $ do + (parse (dialog '[' ']') "(Spec)" "[bonjour *madame*](titi)") + `shouldBe` (Right $ Simple [Raw [Word "bonjour"], + WeakEmph [Word "madame"]] + "titi") + + it "should deal with no second part in with say dialog" $ do + (parse (dialog '[' ']') "(Spec)" "[bonjour, |cria-t-il|](tutu)") + `shouldBe` (Right $ WithSay [Raw [Word "bonjour", Comma]] + [Raw [Word "cria-t-il"]] + [] + "tutu") + + it "should fail when required (1/5)" $ do + (parse (dialog '[' ']') "(Spec)" "[bonjour") + `shouldSatisfy` isLeft + + it "should fail when required (2/5)" $ do + (parse (dialog '[' ']') "(Spec)" "[bonjour |madame") + `shouldSatisfy` isLeft + + it "should fail when required (3/5)" $ do + (parse (dialog '[' ']') "(Spec)" "[bonjour |madame| monsieur") + `shouldSatisfy` isLeft + + it "should fail when required (4/5)" $ do + (parse (dialog '[' ']') "(Spec)" "[bonjour |madame]") + `shouldSatisfy` isLeft + + it "should fail when required (5/5)" $ do + (parse (dialog '[' ']') "(Spec)" "[bonjour madame](truc") + `shouldSatisfy` isLeft + +tellerSpec :: Spec +tellerSpec = do + describe "teller" $ do + it "should parse a list of formats" $ do + (parse teller "(Spec)" "bonjour, comment **allez vous**?") + `shouldBe` (Right . Teller $ [Raw [Word "bonjour", + Comma, + Word "comment"], + StrongEmph [Word "allez", + Word "vous"], + Raw [QuestionMark]]) + + it "should stop when it encounters a dialog marker" $ do + (parse teller "(Spec)" "bonjour: [comment **allez vous**?") + `shouldBe` (Right . Teller $ [Raw [Word "bonjour", Colon]]) + + it "should fail when it encounters a closing dialog marker" $ do + (parse teller "(Spec)" "bonjour: ]comment **allez vous**?") + `shouldSatisfy` isLeft + + it "should fail when it encounters a closing dialog marker" $ do + (parse teller "(Spec)" "bonjour: |comment **allez vous**?") + `shouldSatisfy` isLeft + +componentSpec :: Spec +componentSpec = do + describe "component" $ do + it "should return a teller text" $ do + (parse component "(Spec)" "bonjour, je suis un *homme*! [") + `shouldBe` (Right $ Teller [Raw [Word "bonjour", + Comma, + Word "je", + Word "suis", + Word "un"], + WeakEmph [Word "homme"], + Raw [ExclamationMark]]) + + it "should deal with white spaces" $ do + (parse component "(Spec)" " bonjour, je suis un *homme*! [") + `shouldBe` (Right $ Teller [Raw [Word "bonjour", + Comma, + Word "je", + Word "suis", + Word "un"], + WeakEmph [Word "homme"], + Raw [ExclamationMark]]) + + it "should parse inner dialog" $ do + (parse component "(Spec)" "(chose)") + `shouldBe` (Right . Thought $ WithSay [Raw [Word "Franchement", + SuspensionPoints]] + [Raw [Word "pensa-t-il", Point]] + [] + "chose") + + it "should parse inner dialog with whitespaces" $ do + (parse component "(Spec)" " (chose)") + `shouldBe` (Right . Thought $ WithSay [Raw [Word "Franchement", + SuspensionPoints]] + [Raw [Word "pensa-t-il", Point]] + [] + "chose") + + it "should fail if a dialog is put inside a dialog" $ do + (parse component "(Spec)" "(tété)") + `shouldSatisfy` isLeft + +paragraphSpec :: Spec +paragraphSpec = do + describe "paragraph" $ do + it "should parse a complete paragraph" $ do + parse paragraph "(Spec)" "Il faisait beau. [Salut, |cria l'homme,| comment va ?](homme) Je me tus...." + `shouldBe` (Right $ Story [Teller [Raw [Word "Il", Word "faisait", Word "beau", Point]], + (Audible $ WithSay [Raw [Word "Salut", Comma]] + [Raw [Word "cria", Word "l'homme", Comma]] + [Raw [Word "comment", Word "va", QuestionMark]] + "homme"), + Teller [Raw [Word "Je", Word "me", Word "tus", SuspensionPoints]]]) + + it "should parse a complete paragraph with white spaces" $ do + parse paragraph "(Spec)" " Il faisait beau. [Salut, |cria l'homme,| comment va ?](homme) Je me tus...." + `shouldBe` (Right $ Story [Teller [Raw [Word "Il", Word "faisait", Word "beau", Point]], + (Audible $ WithSay [Raw [Word "Salut", Comma]] + [Raw [Word "cria", Word "l'homme", Comma]] + [Raw [Word "comment", Word "va", QuestionMark]] + "homme"), + Teller [Raw [Word "Je", Word "me", Word "tus", SuspensionPoints]]]) + + it "should fail if needed" $ do + parse paragraph "(Spec)" "Il faisait beau. Salut, |cria l'homme,| comment va ?](homme) Je me tus...." + `shouldSatisfy` isLeft diff --git a/test/Spec.hs b/test/Spec.hs new file mode 100644 index 0000000..a0e92aa --- /dev/null +++ b/test/Spec.hs @@ -0,0 +1,17 @@ +import Test.Hspec + +import Parser + +main :: IO () +main = do + hspec specialcharSpec + hspec wordSpec + hspec textSpec + hspec rawSpec + hspec strongemphSpec + hspec weakemphSpec + hspec formatSpec + hspec dialogSpec + hspec tellerSpec + hspec componentSpec + hspec paragraphSpec