diff --git a/.gitignore b/.gitignore index 2784b39..87db7aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ build/ *.*~ +test/failures +test/*/*/output diff --git a/SSG.ipkg b/SSG.ipkg index 0033da9..f462141 100644 --- a/SSG.ipkg +++ b/SSG.ipkg @@ -13,14 +13,23 @@ authors = "Nathan McCarty" -- langversion -- packages to add to search path -depends = structures +depends = contrib + , structures + , tailrec , eff + , elab-util + , elab-pretty + , sop + , prettier -- modules to install -modules = SSG.Parser.Core - , SSG.Parser.Markdown - , SSG.HTML +modules = SSG.HTML , SSG.HTML.ElementTypes + , SSG.Djot + , SSG.Djot.Lines + , SSG.Djot.Block + , SSG.Djot.Inline + , SSG.Djot.Render -- main file (i.e. file to load at REPL) main = Main diff --git a/examples/src/HelloWorld.idr b/examples/src/HelloWorld.idr index bd272ba..3dc71fa 100644 --- a/examples/src/HelloWorld.idr +++ b/examples/src/HelloWorld.idr @@ -5,15 +5,15 @@ import SSG.HTML import Structures.Dependent.DList helloWorld : Html "html" -helloWorld = - tag "html" ["lang" =$ "en"] [ - tag "head" [] [ - tag "title" [] "Example" - ], - tag "body" [] [ - tag "p" [] [Text "Hello World!"] - ] - ] +-- helloWorld = +-- tag "html" ["lang" =$ "en"] [] [ +-- tag "head" [] [ +-- tag "title" [] "Example" +-- ], +-- tag "body" [] [ +-- tag "p" [] [Text "Hello World!"] +-- ] +-- ] main : IO () main = putStr $ render helloWorld diff --git a/pack.toml b/pack.toml index eed6e77..4a7cd19 100644 --- a/pack.toml +++ b/pack.toml @@ -4,6 +4,11 @@ path = "." ipkg = "SSG.ipkg" test = "test/test.ipkg" +[custom.all.Djot] +type = "local" +path = "." +ipkg = "Djot.ipkg" + [custom.all.SSG-test] type = "local" path = "test" diff --git a/src/SSG/Djot.idr b/src/SSG/Djot.idr new file mode 100644 index 0000000..0fb4599 --- /dev/null +++ b/src/SSG/Djot.idr @@ -0,0 +1,5 @@ +module SSG.Djot + +import public SSG.Djot.Inline +import public SSG.Djot.Block +import public SSG.Djot.Render diff --git a/src/SSG/Djot/Block.idr b/src/SSG/Djot/Block.idr new file mode 100644 index 0000000..64bc98a --- /dev/null +++ b/src/SSG/Djot/Block.idr @@ -0,0 +1,231 @@ +||| DJot Block formatting parser +module SSG.Djot.Block + +import SSG.Djot.Inline +import SSG.Djot.Lines + +import Control.Eff +import Data.Nat +import Data.Maybe +import Data.String +import Derive.Prelude +import Derive.Pretty +import Generics.Derive +import Structures.Dependent.DList + +-- Just for iutils unit tests +import System + +%language ElabReflection +%hide Generics.SOP.values +%hide Generics.Derive.Show +%hide Generics.Derive.Eq + +--****************************** +--* Data Structures * +--****************************** + +||| Types of block level element +public export +data BlockType : Type where + TParagraph : BlockType + THeading : BlockType + +%runElab derive "BlockType" [Generic, Show, Eq, DecEq] + +||| A block element +public export +data Block : BlockType -> Type where + Paragraph : (content : List Inline) -> Block TParagraph + Heading : (level : Nat) -> (content : List Inline) + -> {auto non_zero : IsSucc level} -> {auto in_range : level `LTE` 6} + -> Block THeading + +%runElab derive "Block" [Show, Eq] + +--****************************** +--* Parsing Utilities * +--****************************** + +||| Type alias for block parsing +BParser : Type -> Type +BParser t = Eff [Lines, Fail] t + +-- FIXME: Proper whitespace handling +||| Detect if a line is blank +blankLine : String -> Bool +blankLine = null . trim + +||| Select and apply the first matching parser from a list, with a fallback parser +selectParser : + {fallback_type : BlockType} -> {values : _} + -> (fallback : BParser (Block fallback_type)) + -> DList BlockType (\t => (String -> Bool, BParser (Block t))) values + -> BParser (t : BlockType ** Block t) +selectParser fallback [] = do + x <- fallback + pure (_ ** x) +selectParser fallback ((pred, parser) :: rest) = do + Just line <- peek + | _ => fail + if pred line + then do + x <- parser + pure (_ ** x) + else selectParser fallback rest + +||| Repeat a parser until failure or if we are out of lines +repeat : BParser t -> BParser (List t) +repeat x = do + Just _ <- peek + | _ => pure [] + Just res <- lift . runFail $ x + | _ => pure [] + map (res ::) (repeat x) + +||| Run a parser +runBParser : (input : String) -> BParser t -> Maybe t +runBParser input x = + map fst . extract . runFail . runLines input $ x + +||| Returns true if a character is horizontal whitespace +isHoriz : Char -> Bool +isHoriz ' ' = True +isHoriz '\t' = True +isHoriz _ = False + +--****************************** +--* Syntax * +--****************************** + +-- Forward declare for mutual recursion +||| Top level block parser +pBlock : BParser (t : BlockType ** Block t) + +-- Paragraph + +paragraph : BParser (Block TParagraph) +paragraph = do + contents <- map (inline . joinBy "\n") $ slurp blankLine + pure $ Paragraph contents + +-- Heading + +record HeadingLevel where + constructor MkHL + level : Nat + {auto non_zero : IsSucc level} + {auto in_range : level `LTE` 6} + +acceptHeadingPrefix : String -> Maybe (HeadingLevel, String) +acceptHeadingPrefix str = + let (level, str) = acceptHeadingPrefix' 0 str + in case isItSucc level of + Yes non_zero => + case level `isLTE` 6 of + Yes in_range => Just $ (MkHL level, str) + No _ => Nothing + No _ => Nothing + where + acceptHeadingPrefix' : (acc : Nat) -> String -> (Nat, String) + acceptHeadingPrefix' acc str with (asList str) + acceptHeadingPrefix' acc "" | [] = (acc, "") + acceptHeadingPrefix' acc (strCons '#' str) | ('#' :: x) = + acceptHeadingPrefix' (S acc) str | x + acceptHeadingPrefix' acc (strCons c str) | (c :: x) = + if isHoriz c + then acceptHeadingPrefix' acc str | x + else (acc, strCons c str) + +headingFirstLine : BParser (HeadingLevel, String) +headingFirstLine = do + first <- take >>= fromJust + fromJust $ acceptHeadingPrefix first + +heading : BParser (Block THeading) +heading = do + (MkHL level {non_zero} {in_range}, first) <- headingFirstLine + rest <- slurp blankLine + let contents = joinBy "\n" $ first :: map (stripPrefix level) rest + pure $ Heading level (inline contents) + where + stripPrefix : (l : Nat) -> String -> String + stripPrefix l str with (asList str) + stripPrefix 0 str | _ = str + stripPrefix (S k) "" | [] = "" + stripPrefix (S k) (strCons '#' str) | ('#' :: x) = stripPrefix k str | x + stripPrefix (S k) (strCons c str) | (c :: x) = strCons c str + +-- Definition for top level block parser +pBlock = selectParser paragraph + [ (isJust . acceptHeadingPrefix, heading) + ] + + +--****************************** +--* Top Level Parsing Function * +--****************************** + +||| Parse a string as a sequences of blocks +export +block : (input : String) -> (values ** DList BlockType Block values) +block input = + case runBParser input (repeat pBlock) of + Nothing => (_ ** []) + Just x => fromList x + +--****************************** +--* Unit Tests * +--****************************** + +[myshow] Show (t : BlockType ** Block t) where + show (_ ** block) = show block + +{values : _} -> Show (DList BlockType Block values) where + show xs = + let xs = map (show @{myshow}) $ toList xs + in "[\{joinBy ", " xs}]" + +golden : {values : _} + -> (input : String) -> (ref : DList BlockType Block values) -> IO Bool +golden input ref = do + putStrLn "Input: \{show input}" + putStrLn "Reference:\n \{show ref}" + let (types ** output) = block input + putStrLn "Output:\n \{show output}" + pure $ ref $== output + +-- @@test Paragraph Smoke +smokeParagraph : IO Bool +smokeParagraph = + golden + "Hello World!" + [Paragraph [Text "Hello World!"]] + +-- @@test Two Paragraph Smoke +smokeTwoParagraph : IO Bool +smokeTwoParagraph = + golden + "Hello World!\n\nHello Alice!" + [Paragraph [Text "Hello World!"], Paragraph [Text "Hello Alice!"]] + +-- @@test Heading Smoke +smokeHeading : IO Bool +smokeHeading = + golden + "# Level 1 Heading" + [Heading 1 [Text "Level 1 Heading"]] + +-- @@test Multiline Prefixed Heading Smoke +smokeHeadingMultilinePrefixed : IO Bool +smokeHeadingMultilinePrefixed = + golden + "# Level 1\n# Heading" + [Heading 1 [Text "Level 1", SoftLineBreak, Text "Heading"]] + +-- @@test Multiline Nonprefixed Heading Smoke +smokeHeadingMultilineNonprefixed : IO Bool +smokeHeadingMultilineNonprefixed = + golden + "# Level 1\nHeading" + [Heading 1 [Text "Level 1", SoftLineBreak, Text "Heading"]] diff --git a/src/SSG/Djot/Inline.idr b/src/SSG/Djot/Inline.idr new file mode 100644 index 0000000..d547c43 --- /dev/null +++ b/src/SSG/Djot/Inline.idr @@ -0,0 +1,240 @@ +||| Djot inline formatting parser +module SSG.Djot.Inline + +import Control.Eff +import Data.List1 +import Data.Maybe +import Data.String +import Derive.Prelude +import Derive.Pretty + +-- Just for iutils unit tests +import System + +%language ElabReflection + +--****************************** +--* Data Structures * +--****************************** + +||| Types of inline styling +public export +data Inline : Type where + Text : (text : String) -> Inline + SoftLineBreak : Inline + HardLineBreak : Inline + +%runElab derive "Inline" [Eq, Show, Pretty] + +--****************************** +--* Parsing Utilities * +--****************************** + +||| Type alias for inline parsing +IParser : Type -> Type +IParser t = Eff [State (List Char), Fail] t + +||| Get the next char, modifiying the state +popChar : IParser Char +popChar = do + x :: xs <- get + | [] => fail + put xs + pure x + +||| Get the next char, without modifying the state +peekChar : IParser Char +peekChar = do + x :: xs <- get + | [] => fail + pure x + +||| Attempt to parse something without propagating the failure +try : IParser t -> IParser (Maybe t) +try x = do + state <- get + x <- lift . runFail $ x + case x of + Nothing => do + put state + pure Nothing + Just y => pure $ Just y + + +||| Choose a parser +oneOf : List (IParser t) -> IParser t +oneOf [] = fail +oneOf (x :: xs) = do + state <- get + x <- lift . runFail $ x + case x of + Nothing => do + put state + oneOf xs + Just y => pure y + +||| Parse 0+ of something +many : IParser t -> IParser (List t) +many x = do + Just y <- try x + | _ => pure [] + map (y ::) $ many x + +||| Parse 1+ of something +atLeastOne : IParser t -> IParser (List1 t) +atLeastOne x = do + Just y <- try x + | _ => fail + map (y :::) (many x) + +||| Match exactly the given string +exactly : String -> IParser (List Char) +exactly str = exactly' (unpack str) + where + exactly' : List Char -> IParser (List Char) + exactly' [] = pure [] + exactly' (x :: xs) = do + y <- popChar + if x == y + then map (x ::) (exactly' xs) + else fail + +||| Match vertical whitespace +vert : IParser (List Char) +vert = oneOf + [ exactly "\n" + , exactly "\r\n" + , exactly "\r" + ] + +||| Match horizontal whitespace +horiz : IParser (List Char) +horiz = oneOf + [ exactly " " + , exactly "\t" + ] + +||| Run a parser +runIParser : (str : String) -> IParser t -> Maybe t +runIParser str x = + fst . extract . runState (unpack str) . runFail $ x + +--****************************** +--* Syntax * +--****************************** + +-- Forward declare so we can get mutually recursive +||| Top level parser function for Inline Content +pInline : IParser Inline + +||| Parse a character as plain text +text : IParser Inline +text = do + c <- popChar + pure $ Text (singleton c) + +||| Parse a soft linebreak +softLineBreak : IParser Inline +softLineBreak = do + _ <- atLeastOne singleBreak + pure SoftLineBreak + where + singleBreak : IParser () + singleBreak = do + _ <- many horiz + _ <- vert + _ <- many horiz + pure () + +hardLineBreak : IParser Inline +hardLineBreak = do + _ <- exactly "\\" + _ <- many horiz + _ <- vert + _ <- many horiz + pure HardLineBreak + +-- Definition of pInline +pInline = oneOf + [ hardLineBreak + , softLineBreak + , text + ] + +--****************************** +--* Post Processing * +--****************************** + +||| Combine adjacent Text entries +combineTexts : List Inline -> List Inline +combineTexts [] = [] +combineTexts (Text a :: (Text b :: xs)) = + combineTexts (Text (a ++ b) :: xs) +combineTexts (x :: xs) = x :: combineTexts xs + +||| Remove trailing soft line breaks +removeTrailingSoftbreak : List Inline -> List Inline +removeTrailingSoftbreak xs = reverse (removeTrailingSoftbreak' (reverse xs)) + where + removeTrailingSoftbreak' : List Inline -> List Inline + removeTrailingSoftbreak' (SoftLineBreak :: xs) = removeTrailingSoftbreak' xs + removeTrailingSoftbreak' xs = xs + +||| Top level post processor +postProcess : List Inline -> List Inline +postProcess xs = removeTrailingSoftbreak . combineTexts $ xs + +--****************************** +--* Top Level Parsing Function * +--****************************** + +||| Parse a string as inline content +export +inline : (input : String) -> List Inline +inline input = + postProcess . fromMaybe [] . runIParser input $ many pInline + +--****************************** +--* Unit Tests * +--****************************** + +golden : (input : String) -> (reference : List Inline) -> IO Bool +golden input reference = do + putStrLn "Input: \{show input}" + let opts = Opts 78 + let ref_pretty = Doc.render opts $ pretty reference + putStrLn "Reference:\n\{unlines . map (" " ++) . lines $ ref_pretty}" + let output = inline input + let out_pretty = Doc.render opts $ pretty output + putStrLn "Output:\n\{unlines . map (" " ++) . lines $ out_pretty}" + pure $ reference == output + +-- @@test Just text parses as text +testTextAsText : IO Bool +testTextAsText = + golden "Hello World!" [Text "Hello World!"] + +-- @@test Soft line break smoke +smokeSoftLineBreak : IO Bool +smokeSoftLineBreak = + golden "Hello\nWorld!" [Text "Hello", SoftLineBreak, Text "World!"] + +-- @@test Soft line break double line break +testDoubleSoftLineBreak : IO Bool +testDoubleSoftLineBreak = + golden "Hello\n\nWorld!" [Text "Hello", SoftLineBreak, Text "World!"] + +-- @@test Soft line break trailing line break +testTrailingSoftLineBreak : IO Bool +testTrailingSoftLineBreak = + golden "Hello\n\nWorld!\n" [Text "Hello", SoftLineBreak, Text "World!"] + +-- @@test Soft line break multiple trailing line breaks +testTrailingSoftLineBreaks : IO Bool +testTrailingSoftLineBreaks = + golden "Hello\n\nWorld!\n\n\n" [Text "Hello", SoftLineBreak, Text "World!"] + +-- @@test Hard line break smoke +smokeHardLineBreak : IO Bool +smokeHardLineBreak = + golden "Hello\\\nWorld!" [Text "Hello", HardLineBreak, Text "World!"] diff --git a/src/SSG/Djot/Lines.idr b/src/SSG/Djot/Lines.idr new file mode 100644 index 0000000..1dd7a8b --- /dev/null +++ b/src/SSG/Djot/Lines.idr @@ -0,0 +1,135 @@ +||| An effect for reading an input as a list of lines +module SSG.Djot.Lines + +import Data.String + +import Control.Eff + +-- Only for iutils unit tests +import System + +--************************ +--* Effect Definition * +--************************ + +export +data Lines : Type -> Type where + ||| Peek the next line + Peek : Lines (Maybe String) + ||| Take the next line + Take : Lines (Maybe String) + +--************************ +--* Effect Actions * +--************************ + +export +peek : Has Lines fs => Eff fs (Maybe String) +peek = send Peek + +export +take : Has Lines fs => Eff fs (Maybe String) +take = send Take + +--************************ +--* Extra Effect Actions * +--************************ + +||| Take lines until a line matching the given predicate is encountered, dropping the +||| all of the matching lines until the first non matching one +||| +||| Intended to be used to slurp up to the next blank line, discarding the blanks +export +slurp : Has Lines fs => (predicate : String -> Bool) -> Eff fs (List String) +slurp predicate = do + Just line <- peek + | _ => pure [] + if predicate line + then do + _ <- take + Just next <- peek + | _ => pure [] + if predicate next + then slurp predicate + else pure [] + else do + Just x <- take + | _ => pure [] + map (x ::) (slurp predicate) + +||| Pop the next line and ignore its value +export +drop : Has Lines fs => Eff fs () +drop = do + _ <- take + pure () + +--************************ +--* Effect Handlers * +--************************ + +||| Split the next line from a string +nextLine : String -> Maybe (String, String) +nextLine str = + if null str + then Nothing + else + let + (before, after) = break (\c => any (== c) ['\r', '\n']) str + in Just (before, removeNewline after) + where + -- If `after` is empty, we have hit the end of the string, and there is no newline + -- character to remove. If it has contents, then we need to remove the newline + removeNewline : String -> String + removeNewline str with (strM str) + removeNewline "" | StrNil = "" + removeNewline (strCons '\n' xs) | (StrCons '\n' xs) = xs + -- Handle either a \r or a \r\n + removeNewline (strCons '\r' xs) | (StrCons '\r' xs) with (strM xs) + removeNewline (strCons '\r' "") | (StrCons '\r' "") | StrNil = "" + removeNewline (strCons '\r' _) | (StrCons '\r' _) | (StrCons '\n' xs1) = xs1 + removeNewline (strCons '\r' _) | (StrCons '\r' _) | (StrCons x xs1) = strCons x xs1 + -- We shouldn't ever hit this case, as we would have to have contents after the + -- break, but no newline character, but we fill it in for the sake of totality + removeNewline (strCons x xs) | (StrCons x xs) = str + +unLines : String -> Lines s -> (s, String) +unLines str Peek = + case nextLine str of + Nothing => (Nothing, str) + Just (before, after) => (Just before, str) +unLines str Take = + case nextLine str of + Nothing => (Nothing, str) + Just (before, after) => (Just before, after) + +||| Feed a `Lines` from a provided input string +export +runLines : Has Lines fs => + (input : String) -> Eff fs t -> Eff (fs - Lines) (t, String) +runLines input = + handleRelayS input (\x, y => pure (y, x)) $ \input2, lns, f => + let (vv, input3) = unLines input2 lns + in f input3 vv + +--************************ +--* Unit Tests * +--************************ + +-- @@test runLines Smoke Test +runLinesSmoke : IO Bool +runLinesSmoke = do + let input = "Hello\nWorld\n\n" + putStrLn "Input: \{show input}" + let reference = lines input + putStrLn "Reference: \{show reference}" + let (output, rest) = extract $ runLines input lines' + putStrLn "Output: \{show output}" + putStrLn "Rest: \{show rest}" + pure $ output == reference && null rest + where + lines' : Eff [Lines] (List String) + lines' = do + Just next <- take + | _ => pure [] + map (next ::) lines' diff --git a/src/SSG/Djot/Render.idr b/src/SSG/Djot/Render.idr new file mode 100644 index 0000000..2e7aebd --- /dev/null +++ b/src/SSG/Djot/Render.idr @@ -0,0 +1,57 @@ +||| Render Djot to HTML +module SSG.Djot.Render + +import SSG.HTML +import SSG.Djot.Inline +import SSG.Djot.Block + +import Data.List +import Data.Nat + +||| Render a single inline element as HTML +export +renderInline : Inline -> Maybe (t ** Html t) +-- FIXME: escape text +renderInline (Text text) = Just (_ ** Text text) +renderInline SoftLineBreak = Nothing +renderInline HardLineBreak = Just (_ ** Void "br" []) + +||| Render a list of inline elments to html +export +renderInlines : List Inline -> (types ** DList _ Html types) +renderInlines xs = fromList . catMaybes $ map renderInline xs + +||| Render a single block element as HTML +export +renderBlock : Block t -> (t ** Html t) + +||| Render a list of block level elements to html +export +renderBlocks : {types: _} -> DList _ Block types -> (types' ** DList _ Html types') + +renderBlock (Paragraph content) = + (_ ** Normal "p" [] (snd (renderInlines content))) +renderBlock (Heading 1 content {non_zero = ItIsSucc} {in_range}) = + (_ ** Normal "h1" [] (snd (renderInlines content))) +renderBlock (Heading 2 content {non_zero = ItIsSucc} {in_range}) = + (_ ** Normal "h2" [] (snd (renderInlines content))) +renderBlock (Heading 3 content {non_zero = ItIsSucc} {in_range}) = + (_ ** Normal "h3" [] (snd (renderInlines content))) +renderBlock (Heading 4 content {non_zero = ItIsSucc} {in_range}) = + (_ ** Normal "h4" [] (snd (renderInlines content))) +renderBlock (Heading 5 content {non_zero = ItIsSucc} {in_range}) = + (_ ** Normal "h5" [] (snd (renderInlines content))) +renderBlock (Heading 6 content {non_zero = ItIsSucc} {in_range}) = + (_ ** Normal "h6" [] (snd (renderInlines content))) +renderBlock (Heading (6 + (S n)) content {non_zero = ItIsSucc} {in_range}) = + absurd in_range + +renderBlocks xs = dMap' (\_, x => renderBlock x) xs + +||| Render a complete html document from a list of block level elements +export +renderDocument : {types : _} -> DList _ Block types -> Html "html" +renderDocument xs = + Normal "html" ["lang" =$ "en"] [ + Normal "body" [] (snd (renderBlocks xs)) + ] diff --git a/src/SSG/HTML.idr b/src/SSG/HTML.idr index 91c07f3..7e9a481 100644 --- a/src/SSG/HTML.idr +++ b/src/SSG/HTML.idr @@ -63,8 +63,8 @@ namespace Html if length attributes > 0 then let attrs = joinBy " " $ map toString attributes - in "<\{type} \{attrs} />" - else "<\{type} />" + in "\{indent}<\{type} \{attrs}>" + else "\{indent}<\{type}>" viewIndented indent_level (Normal type attributes {content_types} contents) = let indent = replicate (indent_level * 2) ' ' in -- Special handling if the tag contains exactly one `Text` element, we won't @@ -123,10 +123,10 @@ namespace Html TagType' : (type : String) -> {auto prf : IsValidTag type} -> Type TagType' type {prf} = TagType prf - ||| Smart constructor for tags - public export - tag : (type : String) -> {auto prf : IsValidTag type} -> TagType prf - tag type {prf = (PVoid type)} = Void type - tag type {prf = (PRawText type)} = RawText type - tag type {prf = (PEscapableRawText type)} = RawText type - tag type {prf = (PNormal type)} = Normal type + -- ||| Smart constructor for tags + -- public export + -- tag : (type : String) -> {auto prf : IsValidTag type} -> TagType prf + -- tag type {prf = (PVoid type)} = Void type + -- tag type {prf = (PRawText type)} = RawText type + -- tag type {prf = (PEscapableRawText type)} = RawText type + -- tag type {prf = (PNormal type)} = Normal type diff --git a/src/SSG/Parser/Core.idr b/src/SSG/Parser/Core.idr deleted file mode 100644 index e070a95..0000000 --- a/src/SSG/Parser/Core.idr +++ /dev/null @@ -1 +0,0 @@ -module SSG.Parser.Core diff --git a/src/SSG/Parser/Markdown.idr b/src/SSG/Parser/Markdown.idr deleted file mode 100644 index e760e1b..0000000 --- a/src/SSG/Parser/Markdown.idr +++ /dev/null @@ -1 +0,0 @@ -module SSG.Parser.Markdown diff --git a/test/Main.idr b/test/Main.idr new file mode 100644 index 0000000..04cd676 --- /dev/null +++ b/test/Main.idr @@ -0,0 +1,9 @@ +module Main + +import Test.Golden.RunnerHelper + +main : IO () +main = goldenRunner + [ "Hedgehog Tests" `atDir` "hedgehog" + , "Djot -> HTML Tests" `atDir` "djotToHtml" + ] diff --git a/test/djotToHtml/001-paragraph/Main.idr b/test/djotToHtml/001-paragraph/Main.idr new file mode 100644 index 0000000..64b3cdf --- /dev/null +++ b/test/djotToHtml/001-paragraph/Main.idr @@ -0,0 +1,18 @@ +module Main + +import SSG.Djot +import SSG.HTML + +import System +import System.File + +main : IO () +main = do + Right contents <- readFile "./test.dj" + | Left err => do + printLn err + exitFailure + let (_ ** blocks) = block contents + let html = renderDocument blocks + let output = render html + putStrLn output diff --git a/test/djotToHtml/001-paragraph/expected b/test/djotToHtml/001-paragraph/expected new file mode 100644 index 0000000..612076b --- /dev/null +++ b/test/djotToHtml/001-paragraph/expected @@ -0,0 +1,33 @@ + + +
+Hello World!
+Hello Alice!
++ A Multi-line + Paragraph +
+Two line breaks
++ A multiline paragraph + with some indentation +
+
+ This is a hard
+
+ line break
+
+ And again
+
+ with some spaces afterwards
+
+ And now we
+ mix soft
+
+ and hard
+ linebreaks
+
####### Level 7 Not a heading
+