From 28320d30ed03a693c4d2d1afe939ff8fe57fa4c4 Mon Sep 17 00:00:00 2001 From: Levi Pearson Date: Mon, 24 Feb 2014 01:00:46 -0700 Subject: [PATCH] Initial commit for hasciidoc --- .gitignore | 1 + LICENSE | 30 ++++++++++++ Setup.hs | 2 + asciidoc.md | 120 +++++++++++++++++++++++++++++++++++++++++++++++ hasciidoc.cabal | 40 ++++++++++++++++ src/Hasciidoc.hs | 95 +++++++++++++++++++++++++++++++++++++ tests/Main.hs | 41 ++++++++++++++++ 7 files changed, 329 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Setup.hs create mode 100644 asciidoc.md create mode 100644 hasciidoc.cabal create mode 100644 src/Hasciidoc.hs create mode 100644 tests/Main.hs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a261f29 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cbe3427 --- /dev/null +++ b/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2014, Levi Pearson + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Levi Pearson nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 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/asciidoc.md b/asciidoc.md new file mode 100644 index 0000000..0058cd9 --- /dev/null +++ b/asciidoc.md @@ -0,0 +1,120 @@ +--- +title: AsciiDoc Parsing +... + +# Document Structure + +A *document* is a series of *block elements*. + +The first element is optionally a *document header*. + +The next element is optionally a *preamble*. + +* Document + * Header (optional) + Must be separated from the remainder of the document by one or more + blank lines and cannot contain blank lines. + Can include comments. + Can include attribute entries, typically *doctype*, *lang*, *encoding*, + *icons*, *data-uri*, *toc*, *numbered* + Command-line attributes override header attributes. + * Title + * Info (optional) + * AuthorInfo + Format: firstname[ [middlename ]lastname][ ] + Multi-word names should be separated by underscores, + e.g. van_Gogh. + If the format isn't matched, the entire line becomes a + FirstName. + * FirstName + * OtherNames (optional) + * MiddleName (optional) + * LastName + * EmailAddress (optional) + * RevisionInfo (optional) + * Either + * RevisionNumber (optional) + * RevisionDate + * RevisionRemark (optional) + * Or + * RCS/CVS/SVN $Id$ marker. + The *revnumber*, *revdata*, and *author* attributes are + extracted and set and displayed in the header. + The header author line may be omitted. + + * Preamble (optional) + * SectionBody + * Section (0 or more) + Up to 4 section levels (1-4) in addition to the document title (level 0) + Has attributes *level* and *sectnum* implcitly defined + * Title + * SectionBody (optional) + * One of (repeated 1 or more times) + * Either + * BlockTitle (optional) + Format: . + * Block + * Or + * BlockMacro + * Section (0 or more) + +* Block + Floating titles probably belong in here somewhere. + * One of + * Paragraph + * DelimitedBlock + * List + * One of + * BulletedList + * ListItem (1 or more) + * NumberedList + * ListItem (1 or more) + * CalloutList + * ListItem (1 or more) + * LabeledList + * ListEntry (1 or more) + * ListLabel + * ListTerm (1 or more) + * ListItem + * ItemText + * One of (repeated 0 or more times) + * List + * ListParagraph + A paragraph with *listelement* option set + * ListContinuation + * Table + * Or (these can appear almost anywhere) + * BlockId + Applies to the following *Title*, *Paragraph*, *List*, + *DelimitedBlock*, *Table*, or *BlockMacro* + Format: [[<id>]] + * AttributeEntry + * AttributeList + Applies to the following *Block* + Most blocks support *id*, *role*, and *reftext* attributes + Format: [<attr>(, <attr>)*] + +Notes: + * Block elements are separated by line boundaries + * *Header*, *Title*, *Paragraph*, and *ItemText* cannot contain blank lines. + + +```.haskell +data Document = Doc + { _docAttrs :: Attributes + , _header :: Maybe DocHeader + , _preamble :: Maybe SectionBody + , _sections :: Seq Section + } + + +data Section = Sec + { _secAttrs :: Attributes + , _secTitle :: Text + , _secBody :: Maybe SectionBody + , _secChildren :: Seq Section + } + +type SectionBody = Seq Block + +``` diff --git a/hasciidoc.cabal b/hasciidoc.cabal new file mode 100644 index 0000000..14dfd7f --- /dev/null +++ b/hasciidoc.cabal @@ -0,0 +1,40 @@ +-- Initial hasciidoc.cabal generated by cabal init. For further +-- documentation, see http://haskell.org/cabal/users-guide/ + +name: hasciidoc +version: 0.1.0.0 +synopsis: An asciidoc implementation +-- description: +license: BSD3 +license-file: LICENSE +author: Levi Pearson +maintainer: levipearson@gmail.com +-- copyright: +category: Text +build-type: Simple +-- extra-source-files: +cabal-version: >=1.10 + +library + exposed-modules: Hasciidoc + -- other-modules: + -- other-extensions: + build-depends: base >=4.6 && <4.7 + , attoparsec + , containers + , text + -- hs-source-dirs: + default-language: Haskell2010 + hs-source-dirs: src + +test-suite tests + type: exitcode-stdio-1.0 + main-is: Main.hs + build-depends: base >= 4.6 && < 4.7 + , attoparsec + , text + , hasciidoc + build-depends: tasty + , tasty-hspec + hs-source-dirs: tests + default-language: Haskell2010 \ No newline at end of file diff --git a/src/Hasciidoc.hs b/src/Hasciidoc.hs new file mode 100644 index 0000000..8bc7275 --- /dev/null +++ b/src/Hasciidoc.hs @@ -0,0 +1,95 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# OPTIONS_GHC -fno-warn-unused-do-bind #-} + +module Hasciidoc where + +import Control.Applicative +import Data.Attoparsec.Text +import Data.Char +import Data.Map (Map) +import qualified Data.Map as M +import Data.Sequence (Seq) +import qualified Data.Sequence as S +import Data.Text (Text) +import qualified Data.Text as T + +type Attributes = Map Text Text + +data Document = Doc + { _docAttrs :: Attributes + , _header :: Maybe DocHeader + , _preamble :: Maybe SectionBody + , _sections :: Seq Section + } deriving (Show) + +data DocHeader = DocHead + { _docTitle :: Text + } deriving (Show) + +data Section = Sec + { _secAttrs :: Attributes + , _secTitle :: Text + , _secBody :: Maybe SectionBody + , _secChildren :: Seq Section + } deriving (Show) + +type SectionBody = Seq Block + +data Block = Block + { _blockKind :: BlockKind + , _blockAttrs :: Attributes + , _blockTitle :: Maybe Text + } deriving (Show) + +data BlockKind = Paragraph + | DelimBlock + | List + deriving (Show) + +data Inlines = TextChunk Text + deriving (Show) + +pCountedChar :: Char -> Parser Int +pCountedChar c = ((+) <$> (char c >> pure 1) <*> pCountedChar c) + <|> pure 0 + +collapseSp = T.unwords . T.words + + +pTitle :: Parser (Int, Text) +pTitle = pOneLineTitle <|> pUnderTitle + +pOneLineTitle :: Parser (Int, Text) +pOneLineTitle = (,) <$ char '=' + <*> pCountedChar '=' + <* space + <* skipSpace + <*> (collapseSp <$> takeTill (\c -> c == '=' || isEndOfLine c)) + <* takeTill isEndOfLine + <* satisfy isEndOfLine + <?> "pOneLineTitle" + +pUnderTitle :: Parser (Int, Text) +pUnderTitle = flip (,) <$> (collapseSp <$> takeTill isEndOfLine) + <* satisfy isEndOfLine + <*> pTitleUnderline + <?> "pUnderTitle" + +pTitleUnderline :: Parser Int +pTitleUnderline = do + c <- anyChar + char c + takeTill (not . (== c)) + satisfy isEndOfLine + titleCharLevel c + + +titleCharLevel :: Monad m => Char -> m Int +titleCharLevel c = case c of + '=' -> return 0 + '-' -> return 1 + '~' -> return 2 + '^' -> return 3 + '+' -> return 4 + _ -> fail "Bad title underline character" + diff --git a/tests/Main.hs b/tests/Main.hs new file mode 100644 index 0000000..d7431a8 --- /dev/null +++ b/tests/Main.hs @@ -0,0 +1,41 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Main where + +import Data.Attoparsec.Text +import qualified Data.Text as T + +import Test.Tasty +import Test.Tasty.Hspec as HS + +import Hasciidoc + +main :: IO () +main = defaultMain tests + +tests :: TestTree +tests = testGroup "Tests" [specs] + +-- Hspec Tests + +specs :: TestTree +specs = testGroup "Specifications" + [ HS.testCase "Header Parsing" headerParseSpec + ] + +headerParseSpec :: Spec +headerParseSpec = do + describe "Document Header" $ do + + describe "Document Title" $ do + + describe "pTitle" $ do + it "parses a one-line level 0 title with no right-hand delimiter" $ + parseOnly pTitle "= This is a level 0 title\n" + `shouldBe` + Right (0, "This is a level 0 title") + + it "parses a one-line level 0 title with a right-hand delimeter" $ + parseOnly pTitle "= This is another level 0 title =\n" + `shouldBe` + Right (0, "This is another level 0 title")