packages feed

ema 0.4.0.0 → 0.6.0.0

raw patch · 19 files changed

+140/−467 lines, 19 filesdep +url-slugdep −commonmarkdep −commonmark-extensionsdep −commonmark-pandocPVP ok

version bump matches the API change (PVP)

Dependencies added: url-slug

Dependencies removed: commonmark, commonmark-extensions, commonmark-pandoc, megaparsec, pandoc-types, parsec, parser-combinators, unicode-transforms, unionmount, uri-encode, yaml

API changes (from Hackage documentation)

- Ema: Slug :: Text -> Slug
- Ema: [unSlug] :: Slug -> Text
- Ema: decodeSlug :: Text -> Slug
- Ema: encodeSlug :: Slug -> Text
- Ema: newtype Slug
- Ema: unicodeNormalize :: Text -> Text
- Ema.CLI: instance Data.Constraint.Extras.ArgDict c Ema.CLI.Action
- Ema.CLI: instance Data.GADT.Internal.GCompare Ema.CLI.Action
- Ema.CLI: instance Data.GADT.Internal.GEq Ema.CLI.Action
- Ema.CLI: instance Data.GADT.Internal.GShow Ema.CLI.Action
- Ema.Example.Ex01_HelloWorld: main :: IO ()
- Ema.Example.Ex02_Basic: About :: Route
- Ema.Example.Ex02_Basic: Index :: Route
- Ema.Example.Ex02_Basic: Model :: Text -> Model
- Ema.Example.Ex02_Basic: [unModel] :: Model -> Text
- Ema.Example.Ex02_Basic: data Route
- Ema.Example.Ex02_Basic: instance Ema.Class.Ema Ema.Example.Ex02_Basic.Model Ema.Example.Ex02_Basic.Route
- Ema.Example.Ex02_Basic: instance GHC.Enum.Bounded Ema.Example.Ex02_Basic.Route
- Ema.Example.Ex02_Basic: instance GHC.Enum.Enum Ema.Example.Ex02_Basic.Route
- Ema.Example.Ex02_Basic: instance GHC.Show.Show Ema.Example.Ex02_Basic.Route
- Ema.Example.Ex02_Basic: main :: IO ()
- Ema.Example.Ex02_Basic: newtype Model
- Ema.Example.Ex02_Basic: render :: Some Action -> Model -> Route -> LByteString
- Ema.Example.Ex03_Clock: Index :: Route
- Ema.Example.Ex03_Clock: OnlyTime :: Route
- Ema.Example.Ex03_Clock: data Route
- Ema.Example.Ex03_Clock: instance Ema.Class.Ema Data.Time.Clock.Internal.UTCTime.UTCTime Ema.Example.Ex03_Clock.Route
- Ema.Example.Ex03_Clock: instance GHC.Enum.Bounded Ema.Example.Ex03_Clock.Route
- Ema.Example.Ex03_Clock: instance GHC.Enum.Enum Ema.Example.Ex03_Clock.Route
- Ema.Example.Ex03_Clock: instance GHC.Show.Show Ema.Example.Ex03_Clock.Route
- Ema.Example.Ex03_Clock: main :: IO ()
- Ema.Example.Ex03_Clock: render :: Some Action -> UTCTime -> Route -> LByteString
- Ema.Helper.Blaze: layoutWith :: AttributeValue -> AttributeValue -> Html -> Html -> LByteString
- Ema.Helper.Blaze: tailwind2ShimCdn :: Html
- Ema.Helper.Blaze: twindLayout :: Some Action -> Html -> Html -> LByteString
- Ema.Helper.Blaze: twindShimOfficial :: Html
- Ema.Helper.Blaze: twindShimUnofficial :: Html
- Ema.Helper.Markdown: fullMarkdownSpec :: SyntaxSpec' m il bl => SyntaxSpec m il bl
- Ema.Helper.Markdown: parseMarkdown :: FilePath -> Text -> Either Text Pandoc
- Ema.Helper.Markdown: parseMarkdownWithFrontMatter :: forall meta m il bl. (FromJSON meta, m ~ Either ParseError, bl ~ Cm () Blocks, il ~ Cm () Inlines) => SyntaxSpec m il bl -> FilePath -> Text -> Either Text (Maybe meta, Pandoc)
- Ema.Helper.Markdown: plainify :: [Inline] -> Text
- Ema.Helper.PathTree: treeDeleteChild :: Eq a => a -> [Tree a] -> [Tree a]
- Ema.Helper.PathTree: treeDeleteLeafPath :: Eq a => NonEmpty a -> [Tree a] -> [Tree a]
- Ema.Helper.PathTree: treeDeletePath :: Eq a => NonEmpty a -> [Tree a] -> [Tree a]
- Ema.Helper.PathTree: treeDeletePathWithLastBehavingAs :: forall a. Eq a => (a -> [Tree a] -> [Tree a]) -> NonEmpty a -> [Tree a] -> [Tree a]
- Ema.Helper.PathTree: treeInsertPath :: Eq a => NonEmpty a -> [Tree a] -> [Tree a]
- Ema.Helper.PathTree: treeInsertPathMaintainingOrder :: (Eq a, Ord ord) => (NonEmpty a -> ord) -> NonEmpty a -> [Tree a] -> [Tree a]
+ Ema.CLI: instance Data.GADT.Internal.GCompare @Type Ema.CLI.Action
+ Ema.CLI: instance Data.GADT.Internal.GEq @Type Ema.CLI.Action
+ Ema.CLI: instance Data.GADT.Internal.GShow @Type Ema.CLI.Action
+ Ema.CLI: instance forall (c :: Type -> GHC.Types.Constraint). Data.Constraint.Extras.ArgDict @Type c Ema.CLI.Action

Files

CHANGELOG.md view
@@ -1,5 +1,17 @@ # Revision history for ema +## 0.6.0.0 -- 2022-02-05++- Websocket API: Add `ema.switchRoute` to switch to other routes in live server.+- Smaller core: remove helpers and examples (examples can be enabled by a flag)+  - Helpers+    - `Ema.Helpers.PathTree` moved to separate package *pathtree*.+    - `Ema.Helpers.FileSystem` moved to separate package *unionmount*.+    - `Ema.Helpers.Markdown` moved to separate package *commonmark-simple*.+    - `Ema.Helpers.Blaze` is no more. See `ema-template` if you need a ready made template using blaze HTML and TailwindCSS.+  - Examples `with-examples` cabal flag is made False by default. Examples are not exposed modules.+  - `Ema.Route.Slug` moved to separate package *url-slug*+ ## 0.4.0.0 -- 2022-01-19  - Pin TailwindCSS to 2.x, because the 3.x broke our CDN url
ema.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.4 name:               ema-version:            0.4.0.0+version:            0.6.0.0 license:            AGPL-3.0-only copyright:          2021 Sridhar Ratnakumar maintainer:         srid@srid.ca@@ -22,11 +22,7 @@  flag with-examples   description: Include examples and their dependencies-  default:     True--flag with-helpers-  description: Include helper modules based on blaze-html-  default:     True+  default:     False  library   -- Modules included in this executable, other than Main.@@ -54,33 +50,20 @@     , optparse-applicative     , relude                  >=0.7      && <1.0     , text-    , unicode-transforms-    , unionmount     , unliftio-    , uri-encode+    , url-slug     , wai     , wai-middleware-static     , wai-websockets     , warp     , websockets -  if (flag(with-helpers) || flag(with-examples))+  if flag(with-examples)     build-depends:       , blaze-html       , blaze-markup-      , commonmark-      , commonmark-extensions-      , commonmark-pandoc-      , megaparsec-      , pandoc-types-      , parsec-      , parser-combinators       , time-      , yaml -    if flag(with-examples)-      build-depends: time-   mixins:     base hiding (Prelude),     relude (Relude as Prelude, Relude.Container.One),@@ -88,41 +71,65 @@    ghc-options:     -Wall -Wincomplete-record-updates -Wincomplete-uni-patterns+    -Wmissing-deriving-strategies -Wunused-foralls -Wunused-foralls+    -fprint-explicit-foralls -fprint-explicit-kinds    default-extensions:+    NoStarIsType+    BangPatterns+    ConstraintKinds+    DataKinds+    DeriveDataTypeable+    DeriveFoldable+    DeriveFunctor+    DeriveGeneric+    DeriveLift+    DeriveTraversable+    DerivingStrategies+    DerivingVia+    EmptyCase+    EmptyDataDecls+    EmptyDataDeriving+    ExistentialQuantification+    ExplicitForAll     FlexibleContexts     FlexibleInstances+    GADTSyntax+    GeneralisedNewtypeDeriving+    ImportQualifiedPost     KindSignatures     LambdaCase     MultiParamTypeClasses     MultiWayIf+    NumericUnderscores     OverloadedStrings+    PolyKinds+    PostfixOperators+    RankNTypes     ScopedTypeVariables+    StandaloneDeriving+    StandaloneKindSignatures     TupleSections+    TypeApplications+    TypeFamilies+    TypeOperators     ViewPatterns    exposed-modules:     Ema     Ema.CLI -  if (flag(with-helpers) || flag(with-examples))-    exposed-modules:-      Ema.Helper.Blaze-      Ema.Helper.FileSystem-      Ema.Helper.Markdown-      Ema.Helper.PathTree-   other-modules:     Ema.App     Ema.Asset     Ema.Class     Ema.Generate     Ema.Route-    Ema.Route.Slug     Ema.Server    if flag(with-examples)-    exposed-modules:+    other-modules:+      Ema.Example.Common       Ema.Example.Ex01_HelloWorld       Ema.Example.Ex02_Basic       Ema.Example.Ex03_Clock
src/Ema.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE TypeApplications #-}- module Ema   ( module X,   )@@ -9,7 +7,6 @@ import Ema.Asset as X import Ema.Class as X import Ema.Route as X-import Ema.Route.Slug as X import Ema.Server as X   ( emaErrorHtmlResponse,   )
src/Ema/App.hs view
@@ -1,8 +1,3 @@-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}- module Ema.App   ( runEma,     runEmaPure,@@ -20,14 +15,14 @@   ) import Data.Dependent.Sum (DSum ((:=>))) import Data.LVar (LVar)-import qualified Data.LVar as LVar+import Data.LVar qualified as LVar import Data.Some import Ema.Asset (Asset (AssetGenerated), Format (Html)) import Ema.CLI (Cli)-import qualified Ema.CLI as CLI+import Ema.CLI qualified as CLI import Ema.Class (Ema)-import qualified Ema.Generate as Generate-import qualified Ema.Server as Server+import Ema.Generate qualified as Generate+import Ema.Server qualified as Server import System.Directory (getCurrentDirectory) import UnliftIO   ( BufferMode (BlockBuffering, LineBuffering),
src/Ema/Asset.hs view
@@ -1,6 +1,3 @@-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE TypeApplications #-}- module Ema.Asset where  -- | The type of assets that can be bundled in a static site.@@ -12,7 +9,7 @@     AssetStatic FilePath   | -- | A file whose contents are generated at runtime by user code.     AssetGenerated Format a-  deriving (Eq, Show, Ord, Generic)+  deriving stock (Eq, Show, Ord, Generic)  data Format = Html | Other-  deriving (Eq, Show, Ord, Generic)+  deriving stock (Eq, Show, Ord, Generic)
src/Ema/CLI.hs view
@@ -1,10 +1,6 @@ {-# LANGUAGE ApplicativeDo #-}-{-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE TypeFamilies #-}  module Ema.CLI where @@ -36,7 +32,7 @@ data Cli = Cli   { action :: (Some Action)   }-  deriving (Eq, Show)+  deriving stock (Eq, Show)  cliParser :: Parser Cli cliParser = do
src/Ema/Class.hs view
@@ -1,6 +1,5 @@ {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE TypeApplications #-}  module Ema.Class where 
+ src/Ema/Example/Common.hs view
@@ -0,0 +1,37 @@+module Ema.Example.Common+  ( tailwindLayout,+  )+where++import Text.Blaze.Html.Renderer.Utf8 qualified as RU+import Text.Blaze.Html5 ((!))+import Text.Blaze.Html5 qualified as H+import Text.Blaze.Html5.Attributes qualified as A++-- | A simple and off-the-shelf layout using Tailwind CSS+tailwindLayout :: H.Html -> H.Html -> LByteString+tailwindLayout h b =+  layoutWith "en" "UTF-8" (tailwind2ShimCdn >> h) $+    -- The "overflow-y-scroll" makes the scrollbar visible always, so as to+    -- avoid janky shifts when switching to routes with suddenly scrollable content.+    H.body ! A.class_ "overflow-y-scroll" $ b+  where+    -- A general layout+    layoutWith :: H.AttributeValue -> H.AttributeValue -> H.Html -> H.Html -> LByteString+    layoutWith lang encoding appHead appBody = RU.renderHtml $ do+      H.docType+      H.html ! A.lang lang $ do+        H.head $ do+          H.meta ! A.charset encoding+          -- This makes the site mobile friendly by default.+          H.meta ! A.name "viewport" ! A.content "width=device-width, initial-scale=1"+          appHead+        appBody++    -- Loads full tailwind CSS from CDN (not good for production)+    tailwind2ShimCdn :: H.Html+    tailwind2ShimCdn =+      H.link+        ! A.href "https://unpkg.com/tailwindcss@2/dist/tailwind.min.css"+        ! A.rel "stylesheet"+        ! A.type_ "text/css"
src/Ema/Example/Ex01_HelloWorld.hs view
@@ -1,5 +1,3 @@-{-# LANGUAGE TypeApplications #-}- -- | The simplest Ema site possible. -- -- A site with one route (index) that displays content generated from pure
src/Ema/Example/Ex02_Basic.hs view
@@ -1,24 +1,20 @@-{-# LANGUAGE TypeApplications #-}- -- | A very simple site with two routes, and HTML rendered using Blaze DSL module Ema.Example.Ex02_Basic where  import Control.Concurrent (threadDelay)-import qualified Data.LVar as LVar-import Data.Some (Some (..))+import Data.LVar qualified as LVar import Ema (Ema (..))-import qualified Ema-import qualified Ema.CLI-import qualified Ema.CLI as CLI-import qualified Ema.Helper.Blaze as EB+import Ema qualified+import Ema.CLI qualified as CLI+import Ema.Example.Common (tailwindLayout) import Text.Blaze.Html5 ((!))-import qualified Text.Blaze.Html5 as H-import qualified Text.Blaze.Html5.Attributes as A+import Text.Blaze.Html5 qualified as H+import Text.Blaze.Html5.Attributes qualified as A  data Route   = Index   | About-  deriving (Show, Enum, Bounded)+  deriving stock (Show, Enum, Bounded)  newtype Model = Model {unModel :: Text} @@ -35,14 +31,14 @@ main :: IO () main = do   void $-    Ema.runEma (\act m -> Ema.AssetGenerated Ema.Html . render act m) $ \act model -> do+    Ema.runEma (\_act m -> Ema.AssetGenerated Ema.Html . render m) $ \act model -> do       LVar.set model $ Model "Hello World. "       when (CLI.isLiveServer act) $         liftIO $ threadDelay maxBound -render :: Some Ema.CLI.Action -> Model -> Route -> LByteString-render emaAction model r =-  EB.twindLayout emaAction (H.title "Basic site" >> H.base ! A.href "/") $+render :: Model -> Route -> LByteString+render model r =+  tailwindLayout (H.title "Basic site" >> H.base ! A.href "/") $     H.div ! A.class_ "container mx-auto" $ do       H.div ! A.class_ "mt-8 p-2 text-center" $ do         case r of
src/Ema/Example/Ex03_Clock.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -fno-warn-orphans #-}  -- | A very simple site with routes, but based on dynamically changing values@@ -10,22 +9,20 @@ module Ema.Example.Ex03_Clock where  import Control.Concurrent (threadDelay)-import qualified Data.LVar as LVar+import Data.LVar qualified as LVar import Data.List ((!!))-import Data.Some (Some) import Data.Time (UTCTime, defaultTimeLocale, formatTime, getCurrentTime) import Ema (Ema (..))-import qualified Ema-import qualified Ema.CLI-import qualified Ema.Helper.Blaze as EB+import Ema qualified+import Ema.Example.Common (tailwindLayout) import Text.Blaze.Html5 ((!))-import qualified Text.Blaze.Html5 as H-import qualified Text.Blaze.Html5.Attributes as A+import Text.Blaze.Html5 qualified as H+import Text.Blaze.Html5.Attributes qualified as A  data Route   = Index   | OnlyTime-  deriving (Show, Enum, Bounded)+  deriving stock (Show, Enum, Bounded)  instance Ema UTCTime Route where   encodeRoute _time = \case@@ -39,15 +36,15 @@ main :: IO () main = do   void $-    Ema.runEma (\act m -> Ema.AssetGenerated Ema.Html . render act m) $ \_act model ->+    Ema.runEma (\_act m -> Ema.AssetGenerated Ema.Html . render m) $ \_act model ->       forever $ do         -- logDebugNS "ex:clock" "Refreshing time"         LVar.set model =<< liftIO getCurrentTime         liftIO $ threadDelay 1000000 -render :: Some Ema.CLI.Action -> UTCTime -> Route -> LByteString-render emaAction now r =-  EB.twindLayout emaAction (H.title "Clock" >> H.base ! A.href "/") $+render :: UTCTime -> Route -> LByteString+render now r =+  tailwindLayout (H.title "Clock" >> H.base ! A.href "/") $     H.div ! A.class_ "container mx-auto" $ do       H.div ! A.class_ "mt-8 p-2 text-center" $ do         case r of
src/Ema/Generate.hs view
@@ -1,6 +1,5 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE TypeApplications #-}  module Ema.Generate where @@ -70,7 +69,8 @@       writeFileLBS nojekyll ""  newtype StaticAssetMissing = StaticAssetMissing FilePath-  deriving (Show, Exception)+  deriving stock (Show)+  deriving anyclass (Exception)  copyDirRecursively ::   ( MonadIO m,
− src/Ema/Helper/Blaze.hs
@@ -1,93 +0,0 @@-{-# LANGUAGE GADTs #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TypeApplications #-}---- | Use Tailwind CSS with blaze-html? Try this module for rapid prototyping of--- websites in Ema.-module Ema.Helper.Blaze-  ( -- * Main functions-    layoutWith,-    twindLayout,--    -- * Tailwind official shims-    tailwind2ShimCdn,--    -- * Twind.dev shims-    twindShimOfficial,-    twindShimUnofficial,-  )-where--import Data.Some (Some (Some))-import qualified Ema.CLI-import NeatInterpolation (text)-import qualified Text.Blaze.Html.Renderer.Utf8 as RU-import Text.Blaze.Html5 ((!))-import qualified Text.Blaze.Html5 as H-import qualified Text.Blaze.Html5.Attributes as A---- | A general layout-layoutWith :: H.AttributeValue -> H.AttributeValue -> H.Html -> H.Html -> LByteString-layoutWith lang encoding appHead appBody = RU.renderHtml $ do-  H.docType-  H.html ! A.lang lang $ do-    H.head $ do-      H.meta ! A.charset encoding-      -- This makes the site mobile friendly by default.-      H.meta ! A.name "viewport" ! A.content "width=device-width, initial-scale=1"-      appHead-    appBody---- | A simple and off-the-shelf layout using Tailwind CSS-twindLayout :: Some Ema.CLI.Action -> H.Html -> H.Html -> LByteString-twindLayout action h b =-  layoutWith "en" "UTF-8" (shim >> h) $-    -- The "overflow-y-scroll" makes the scrollbar visible always, so as to-    -- avoid janky shifts when switching to routes with suddenly scrollable content.-    H.body ! A.class_ "overflow-y-scroll" $ b-  where-    shim :: H.Html-    shim =-      case action of-        Some (Ema.CLI.Generate _) ->-          twindShimUnofficial-        _ ->-          -- Twind shim doesn't reliably work in dev server mode. Let's just use the-          -- tailwind CDN.-          tailwind2ShimCdn---- | Loads full tailwind CSS from CDN (not good for production)-tailwind2ShimCdn :: H.Html-tailwind2ShimCdn =-  H.link-    ! A.href "https://unpkg.com/tailwindcss@2/dist/tailwind.min.css"-    ! A.rel "stylesheet"-    ! A.type_ "text/css"---- | This shim may not work with hot reload.-twindShimOfficial :: H.Html-twindShimOfficial =-  H.unsafeByteString . encodeUtf8 $-    [text|-    <script type="module" src="https://cdn.skypack.dev/twind/shim"></script>-    |]---- | This shim does work with hot reload, but it spams console with warnings.-twindShimUnofficial :: H.Html-twindShimUnofficial = do-  H.script-    ! A.type_ "text/javascript"-    ! A.src "https://cdn.jsdelivr.net/combine/npm/twind/twind.umd.min.js,npm/twind/observe/observe.umd.min.js"-    $ ""-  H.script ! A.type_ "text/javascript" $ twindShimUnofficialEval-  where-    twindShimUnofficialEval :: H.Html-    twindShimUnofficialEval =-      H.unsafeByteString . encodeUtf8 $-        [text|-        // Be silent to avoid complaining about non-tailwind classes-        // https://github.com/tw-in-js/twind/discussions/180#discussioncomment-678272-        console.log("ema: Twind: setup & observe")-        twind.setup({mode: 'silent'})-        window.emaTwindObs = twindObserve.observe(document.documentElement);-        |]
− src/Ema/Helper/FileSystem.hs
@@ -1,9 +0,0 @@-{-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE TypeApplications #-}--module Ema.Helper.FileSystem-  ( module X,-  )-where--import System.UnionMount as X
− src/Ema/Helper/Markdown.hs
@@ -1,139 +0,0 @@-{-# LANGUAGE ConstraintKinds #-}-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE UndecidableInstances #-}---- | Helper to deal with Markdown files------ TODO: Publish this eventually to Hackage, along with wiki-link stuff from--- emanote (maybe as separate package).-module Ema.Helper.Markdown-  ( -- Parsing-    -- TODO: Publish to Hackage as commonmark-pandoc-simple?-    parseMarkdownWithFrontMatter,-    parseMarkdown,-    fullMarkdownSpec,-    -- Utilities-    plainify,-  )-where--import qualified Commonmark as CM-import qualified Commonmark.Extensions as CE-import qualified Commonmark.Pandoc as CP-import Control.Monad.Combinators (manyTill)-import Data.Aeson (FromJSON)-import qualified Data.Yaml as Y-import qualified Text.Megaparsec as M-import qualified Text.Megaparsec.Char as M-import qualified Text.Pandoc.Builder as B-import Text.Pandoc.Definition (Pandoc (..))-import qualified Text.Pandoc.Walk as W---- | Parse a Markdown file using commonmark-hs with all extensions enabled-parseMarkdownWithFrontMatter ::-  forall meta m il bl.-  ( FromJSON meta,-    m ~ Either CM.ParseError,-    bl ~ CP.Cm () B.Blocks,-    il ~ CP.Cm () B.Inlines-  ) =>-  CM.SyntaxSpec m il bl ->-  -- | Path to file associated with this Markdown-  FilePath ->-  -- | Markdown text to parse-  Text ->-  Either Text (Maybe meta, Pandoc)-parseMarkdownWithFrontMatter spec fn s = do-  (mMeta, markdown) <- partitionMarkdown fn s-  mMetaVal <- first show $ (Y.decodeEither' . encodeUtf8) `traverse` mMeta-  blocks <- first show $ join $ CM.commonmarkWith @(Either CM.ParseError) spec fn markdown-  let doc = Pandoc mempty $ B.toList . CP.unCm @() @B.Blocks $ blocks-  pure (mMetaVal, doc)--parseMarkdown :: FilePath -> Text -> Either Text Pandoc-parseMarkdown fn s = do-  cmBlocks <- first show $ join $ CM.commonmarkWith @(Either CM.ParseError) fullMarkdownSpec fn s-  let blocks = B.toList . CP.unCm @() @B.Blocks $ cmBlocks-  pure $ Pandoc mempty blocks--type SyntaxSpec' m il bl =-  ( Monad m,-    CM.IsBlock il bl,-    CM.IsInline il,-    Typeable m,-    Typeable il,-    Typeable bl,-    CE.HasEmoji il,-    CE.HasStrikethrough il,-    CE.HasPipeTable il bl,-    CE.HasTaskList il bl,-    CM.ToPlainText il,-    CE.HasFootnote il bl,-    CE.HasMath il,-    CE.HasDefinitionList il bl,-    CE.HasDiv bl,-    CE.HasQuoted il,-    CE.HasSpan il-  )---- | GFM + official commonmark extensions-fullMarkdownSpec ::-  SyntaxSpec' m il bl =>-  CM.SyntaxSpec m il bl-fullMarkdownSpec =-  mconcat-    [ CE.gfmExtensions,-      CE.fancyListSpec,-      CE.footnoteSpec,-      CE.mathSpec,-      CE.smartPunctuationSpec,-      CE.definitionListSpec,-      CE.attributesSpec,-      CE.rawAttributeSpec,-      CE.fencedDivSpec,-      CE.bracketedSpanSpec,-      CE.autolinkSpec,-      CM.defaultSyntaxSpec,-      -- as the commonmark documentation states, pipeTableSpec should be placed after-      -- fancyListSpec and defaultSyntaxSpec to avoid bad results when parsing-      -- non-table lines-      CE.pipeTableSpec-    ]---- | Identify metadata block at the top, and split it from markdown body.------ FIXME: https://github.com/srid/neuron/issues/175-partitionMarkdown :: FilePath -> Text -> Either Text (Maybe Text, Text)-partitionMarkdown =-  parse (M.try splitP <|> fmap (Nothing,) M.takeRest)-  where-    separatorP :: M.Parsec Void Text ()-    separatorP =-      void $ M.string "---" <* M.eol-    splitP :: M.Parsec Void Text (Maybe Text, Text)-    splitP = do-      separatorP-      a <- toText <$> manyTill M.anySingle (M.try $ M.eol *> separatorP)-      b <- M.takeRest-      pure (Just a, b)-    parse :: M.Parsec Void Text a -> String -> Text -> Either Text a-    parse p fn s =-      first (toText . M.errorBundlePretty) $-        M.parse (p <* M.eof) fn s---- | Convert Pandoc AST inlines to raw text.-plainify :: [B.Inline] -> Text-plainify = W.query $ \case-  B.Str x -> x-  B.Code _attr x -> x-  B.Space -> " "-  B.SoftBreak -> " "-  B.LineBreak -> " "-  B.RawInline _fmt s -> s-  B.Math _mathTyp s -> s-  -- Ignore the rest of AST nodes, as they are recursively defined in terms of-  -- `Inline` which `W.query` will traverse again.-  _ -> ""
− src/Ema/Helper/PathTree.hs
@@ -1,78 +0,0 @@-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE TypeFamilies #-}---- | Helper to deal with slug trees-module Ema.Helper.PathTree where--import qualified Data.List as List-import qualified Data.List.NonEmpty as NE-import Data.Tree (Tree (Node))-import qualified Data.Tree as Tree---- ---------------------- Data.Tree helpers--- ---------------------treeInsertPath :: Eq a => NonEmpty a -> [Tree a] -> [Tree a]-treeInsertPath =-  treeInsertPathMaintainingOrder void---- | Insert a node by path into a tree with descendants that are ordered.------ Insertion will guarantee that descendants continue to be ordered as expected.------ The order of descendents is determined by the given order function, which--- takes the path to a node and return that node's order. The intention is to--- lookup the actual order value which exists *outside* of the tree--- datastructure itself.-treeInsertPathMaintainingOrder :: (Eq a, Ord ord) => (NonEmpty a -> ord) -> NonEmpty a -> [Tree a] -> [Tree a]-treeInsertPathMaintainingOrder ordF path t =-  orderedTreeInsertPath ordF (toList path) t []-  where-    orderedTreeInsertPath :: (Eq a, Ord b) => (NonEmpty a -> b) -> [a] -> [Tree a] -> [a] -> [Tree a]-    orderedTreeInsertPath _ [] trees _ =-      trees-    orderedTreeInsertPath pathOrder (top : rest) trees ancestors =-      case treeFindChild top trees of-        Nothing ->-          let newChild = Node top $ orderedTreeInsertPath pathOrder rest [] (top : ancestors)-           in sortChildrenOn pathOrder (trees <> one newChild)-        Just (Node _match grandChildren) ->-          let oneDead = treeDeleteChild top trees-              newChild = Node top $ orderedTreeInsertPath pathOrder rest grandChildren (top : ancestors)-           in sortChildrenOn pathOrder (oneDead <> one newChild)-      where-        treeFindChild x xs =-          List.find (\n -> Tree.rootLabel n == x) xs-        sortChildrenOn f =-          sortOn $ (\s -> f $ NE.reverse $ s :| ancestors) . Tree.rootLabel--treeDeletePath :: Eq a => NonEmpty a -> [Tree a] -> [Tree a]-treeDeletePath =-  treeDeletePathWithLastBehavingAs $ \lastInPath ts ->-    List.deleteBy (\x y -> Tree.rootLabel x == Tree.rootLabel y) (Node lastInPath []) ts--treeDeleteLeafPath :: Eq a => NonEmpty a -> [Tree a] -> [Tree a]-treeDeleteLeafPath =-  treeDeletePathWithLastBehavingAs $ \lastInPath ts ->-    case ts of-      [t] -> [t | Tree.rootLabel t /= lastInPath]-      _ -> ts--treeDeletePathWithLastBehavingAs :: forall a. Eq a => (a -> [Tree a] -> [Tree a]) -> NonEmpty a -> [Tree a] -> [Tree a]-treeDeletePathWithLastBehavingAs f slugs =-  go (toList slugs)-  where-    go :: [a] -> [Tree a] -> [Tree a]-    go [] t = t-    go [p] ts =-      f p ts-    go (p : ps) t =-      t <&> \node@(Node x xs) ->-        if x == p-          then Node x $ go ps xs-          else node--treeDeleteChild :: Eq a => a -> [Tree a] -> [Tree a]-treeDeleteChild x =-  List.deleteBy (\p q -> Tree.rootLabel p == Tree.rootLabel q) (Node x [])
src/Ema/Route.hs view
@@ -1,6 +1,3 @@-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TypeApplications #-}- module Ema.Route   ( routeUrl,     routeUrlWith,@@ -10,16 +7,15 @@  import Data.Aeson (FromJSON (parseJSON), Value) import Data.Aeson.Types (Parser)-import qualified Data.List.NonEmpty as NE-import qualified Data.Text as T+import Data.List.NonEmpty qualified as NE+import Data.Text qualified as T import Ema.Class (Ema (encodeRoute))-import Ema.Route.Slug (unicodeNormalize)-import qualified Network.URI.Encode as UE+import Network.URI.Slug qualified as Slug  data UrlStrategy   = UrlPretty   | UrlDirect-  deriving (Eq, Show, Ord)+  deriving stock (Eq, Show, Ord)  instance FromJSON UrlStrategy where   parseJSON val =@@ -43,14 +39,15 @@     relUrlFromPath fp =       case T.stripSuffix (urlStrategySuffix urlStrategy) (toText fp) of         Just htmlFp ->-          case nonEmpty (UE.encodeText . unicodeNormalize <$> T.splitOn "/" htmlFp) of+          case nonEmpty (urlSlugFromText <$> T.splitOn "/" htmlFp) of             Nothing ->               ""             Just (removeLastIf "index" -> partsSansIndex) ->               T.intercalate "/" partsSansIndex         Nothing ->-          T.intercalate "/" $ UE.encodeText . unicodeNormalize <$> T.splitOn "/" (toText fp)+          T.intercalate "/" $ urlSlugFromText <$> T.splitOn "/" (toText fp)       where+        urlSlugFromText = Slug.encodeSlug . fromString @Slug.Slug . toString         removeLastIf :: Eq a => a -> NonEmpty a -> [a]         removeLastIf x xs =           if NE.last xs == x
− src/Ema/Route/Slug.hs
@@ -1,38 +0,0 @@-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE InstanceSigs #-}--module Ema.Route.Slug where--import Data.Aeson (FromJSON, ToJSON)-import Data.Data (Data)-import qualified Data.Text as T-import qualified Data.Text.Normalize as UT-import qualified Network.URI.Encode as UE---- | An URL path is made of multiple slugs, separated by '/'-newtype Slug = Slug {unSlug :: Text}-  deriving (Eq, Show, Ord, Data, Generic, ToJSON, FromJSON)---- | Decode an URL component into a `Slug` using `Network.URI.Encode`-decodeSlug :: Text -> Slug-decodeSlug =-  fromString . UE.decode . toString---- | Encode a `Slug` into an URL component using `Network.URI.Encode`-encodeSlug :: Slug -> Text-encodeSlug =-  UE.encodeText . unSlug--instance IsString Slug where-  fromString :: HasCallStack => String -> Slug-  fromString (toText -> s) =-    if "/" `T.isInfixOf` s-      then error ("Slug cannot contain a slash: " <> s)-      else Slug (unicodeNormalize s)---- Normalize varying non-ascii strings (in filepaths / slugs) to one--- representation, so that they can be reliably linked to.-unicodeNormalize :: Text -> Text-unicodeNormalize = UT.normalize UT.NFC . toText
src/Ema/Server.hs view
@@ -1,7 +1,4 @@-{-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TypeApplications #-}  module Ema.Server where @@ -10,19 +7,19 @@ import Control.Monad.Logger import Data.Default import Data.LVar (LVar)-import qualified Data.LVar as LVar-import qualified Data.Text as T+import Data.LVar qualified as LVar+import Data.Text qualified as T import Ema.Asset import Ema.Class (Ema (..)) import GHC.IO.Unsafe (unsafePerformIO) import NeatInterpolation (text)-import qualified Network.HTTP.Types as H-import qualified Network.Wai as Wai-import qualified Network.Wai.Handler.Warp as Warp-import qualified Network.Wai.Handler.WebSockets as WaiWs-import qualified Network.Wai.Middleware.Static as Static+import Network.HTTP.Types qualified as H+import Network.Wai qualified as Wai+import Network.Wai.Handler.Warp qualified as Warp+import Network.Wai.Handler.WebSockets qualified as WaiWs+import Network.Wai.Middleware.Static qualified as Static import Network.WebSockets (ConnectionException)-import qualified Network.WebSockets as WS+import Network.WebSockets qualified as WS import System.FilePath ((</>)) import Text.Printf (printf) import UnliftIO (MonadUnliftIO)@@ -61,9 +58,9 @@           & Warp.setHost (fromString . toString . unHost $ host)   logger <- askLoggerIO -  logInfoN "============================================"-  logInfoN $ "Running live server at http://" <> unHost host <> ":" <> show port-  logInfoN "============================================"+  logInfoN "=============================================="+  logInfoN $ "Ema live server running: http://" <> unHost host <> ":" <> show port+  logInfoN "=============================================="   liftIO $     Warp.runSettings settings $       assetsMiddleware $@@ -302,6 +299,7 @@            function switchRoute(path) {              console.log(`ema: → Switching to $${path}`);+             window.history.pushState({}, "", path);              sendObservePath(path);           } @@ -309,7 +307,6 @@               const origin = e.target.closest("a");               if (origin) {                 if (window.location.host === origin.host && origin.getAttribute("target") != "_blank") {-                  window.history.pushState({}, "", origin.pathname);                   switchRoute(origin.pathname);                   e.preventDefault();                 };@@ -371,6 +368,11 @@           // the addressback, which has the effect of loading it immediately.           window.onpopstate = function(e) {             watchCurrentRoute();+          };++          // API for user invocations +          window.ema = {+            switchRoute: switchRoute           };         };