Skip to content

Commit 46aad40

Browse files
author
Lucas V. R
committed
Add Ex06_Markdown
1 parent b89df34 commit 46aad40

6 files changed

Lines changed: 191 additions & 0 deletions

File tree

ema-examples/ema-examples.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ library
126126
Ema.Example.Ex03_Store
127127
Ema.Example.Ex04_Multi
128128
Ema.Example.Ex05_MultiRoute
129+
Ema.Example.Ex06_Markdown
129130

130131
hs-source-dirs: src
131132
default-language: Haskell2010
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
{-# LANGUAGE BlockArguments #-}
2+
{-# LANGUAGE QuasiQuotes #-}
3+
{-# LANGUAGE TemplateHaskell #-}
4+
{-# LANGUAGE UndecidableInstances #-}
5+
6+
{- | A very simple markdown website using Ema.Route.Lib.Extra.PandocRoute.
7+
8+
Also demostrates how to set up a custom server for following the currently open
9+
note, using a websocket for editor integration.
10+
-}
11+
module Ema.Example.Ex06_Markdown where
12+
13+
import Control.Monad.Logger (LogLevel (..), MonadLoggerIO (..), defaultLoc, logInfoNS)
14+
import Control.Monad.Logger.Extras (runLoggerLoggingT)
15+
import Data.Default (Default (..))
16+
import Data.Dependent.Sum (DSum (..))
17+
import Data.Generics.Sum.Any
18+
import Data.Map (member)
19+
import Ema
20+
import Ema.CLI qualified as CLI
21+
import Ema.Route.Generic.TH
22+
import Ema.Route.Lib.Extra.PandocRoute qualified as Pandoc
23+
import Ema.Server (EmaServerOptions (..), EmaWsHandler, defaultEmaWsHandler, wsClientJS)
24+
import Network.WebSockets qualified as WS
25+
import Optics.Core ((%))
26+
import System.Directory (makeAbsolute)
27+
import System.FilePath (isAbsolute, isRelative, makeRelative)
28+
import Text.Blaze.Html.Renderer.Utf8 qualified as RU
29+
import Text.Blaze.Html5 ((!))
30+
import Text.Blaze.Html5 qualified as H
31+
import Text.Blaze.Html5.Attributes qualified as A
32+
import UnliftIO.Async (race)
33+
import UnliftIO.STM (TChan, dupTChan, newBroadcastTChanIO, readTChan, writeTChan)
34+
35+
data Arg = Arg
36+
{ pandocArg :: Pandoc.Arg
37+
, editorWsAddress :: String
38+
, editorWsPort :: Int
39+
}
40+
deriving stock (Generic)
41+
42+
instance Default Arg where
43+
def =
44+
Arg
45+
{ pandocArg =
46+
def
47+
{ Pandoc.argBaseDir = "src/Ema/Example/Ex06_Markdown"
48+
}
49+
, editorWsAddress = "127.0.0.1"
50+
, editorWsPort = 9160
51+
}
52+
53+
data Model = Model
54+
{ pandocModel :: Pandoc.Model
55+
, wsNextRoute :: TChan Route
56+
}
57+
deriving stock (Generic)
58+
59+
newtype Route = Route Pandoc.PandocRoute
60+
deriving stock (Show, Eq, Ord, Generic)
61+
62+
deriveGeneric ''Route
63+
deriveIsRoute
64+
''Route
65+
[t|
66+
'[ WithModel Model
67+
, WithSubRoutes
68+
'[ Pandoc.PandocRoute
69+
]
70+
]
71+
|]
72+
73+
instance EmaSite Route where
74+
type SiteArg Route = Arg
75+
76+
siteInput act arg = do
77+
pandocDyn <- siteInput @Pandoc.PandocRoute act (pandocArg arg)
78+
editorWsDyn <- wsConnDyn arg
79+
return $ Model <$> pandocDyn <*> editorWsDyn
80+
81+
siteOutput rp m (Route r) = do
82+
(pandoc, write) <- siteOutput (rp % _As @"Route") (pandocModel m) r
83+
let head' = H.title "Basic site" >> H.base ! A.href "/"
84+
body :: Text = coerce $ write pandoc
85+
html = RU.renderHtml do
86+
H.docType
87+
H.html ! A.lang "en" $ do
88+
H.head do
89+
H.meta ! A.charset "UTF-8"
90+
H.meta ! A.name "viewport" ! A.content "width=device-width, initial-scale=1"
91+
head'
92+
H.body $ H.preEscapedToHtml body
93+
return $ AssetGenerated Html html
94+
95+
wsConnDyn :: forall m. (MonadLoggerIO m) => Arg -> m (Dynamic m (TChan Route))
96+
wsConnDyn arg = do
97+
value <- newBroadcastTChanIO
98+
let manage :: m ()
99+
manage = do
100+
logger <- askLoggerIO
101+
let log = logger defaultLoc "wsConnDyn" LevelInfo
102+
liftIO $ WS.runServer (editorWsAddress arg) (editorWsPort arg) \pendingConn -> do
103+
conn :: WS.Connection <- WS.acceptRequest pendingConn
104+
log "websocket connected"
105+
WS.withPingThread conn 30 pass $
106+
void $ infinitely do
107+
msg <- liftIO $ toString @Text <$> WS.receiveData conn
108+
log $ "got message: " <> show msg
109+
baseDir <- makeAbsolute (Pandoc.argBaseDir $ pandocArg arg)
110+
let fp = makeRelative baseDir msg
111+
case Pandoc.mkPandocRoute fp of
112+
Just (_, route)
113+
-- We should have received an absolute file path inside the base dir
114+
| isAbsolute msg && isRelative fp ->
115+
atomically $ writeTChan value (Route route)
116+
_ -> pass
117+
return $ Dynamic (value, const manage)
118+
119+
main :: IO ()
120+
main = runWithFollow def
121+
122+
runWithFollow ::
123+
SiteArg Route ->
124+
IO ()
125+
runWithFollow input = do
126+
cli <- CLI.cliAction
127+
result <- snd <$> runSiteWithServerOpts @Route followServerOptions cli input
128+
case result of
129+
CLI.Run _ :=> Identity () ->
130+
flip runLoggerLoggingT (CLI.getLogger cli) $
131+
CLI.crash "ema" "Live server unexpectedly stopped"
132+
CLI.Generate _ :=> Identity _ -> pass
133+
134+
followServerOptions :: EmaServerOptions Route
135+
followServerOptions = EmaServerOptions wsClientJS followServerHandler
136+
137+
followServerHandler :: EmaWsHandler Route
138+
followServerHandler conn model =
139+
either id id <$> race (defaultEmaWsHandler @() conn ()) followHandler
140+
where
141+
rp = fromPrism_ $ routePrism model
142+
log = logInfoNS "followServerHandler"
143+
followHandler = do
144+
listenerChan <- atomically $ dupTChan $ wsNextRoute model
145+
route <- atomically $ readTChan listenerChan
146+
let Route pRoute = route
147+
path = routeUrl rp route
148+
if pRoute `member` Pandoc.modelPandocs (pandocModel model)
149+
then do
150+
log $ "switching to " <> show pRoute
151+
liftIO $ WS.sendTextData conn $ "SWITCH " <> path
152+
else log $ "invalid route " <> show pRoute
153+
followHandler
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# This is index
2+
3+
- [go to test](test)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
;;; -*- lexical-binding: t; -*-
2+
;;;
3+
;;; Example "open in ema" command for Ex06_Markdown
4+
5+
(defvar ema-ws-address "ws://127.0.0.1:9160")
6+
7+
(defvar ema-ws--conn nil)
8+
9+
(defun ema-ws-connect ()
10+
(interactive)
11+
(require 'websocket)
12+
(unless ema-ws--conn
13+
(websocket-open
14+
ema-ws-address
15+
:on-open (lambda (ws) (message "ema ws: connected") (setq ema-ws--conn ws))
16+
:on-close (lambda (_) (message "ema ws: disconnected") (setq ema-ws--conn nil)))))
17+
18+
(defun ema-ws-disconnect ()
19+
(interactive)
20+
(require 'websocket)
21+
(when ema-ws--conn (websocket-close ema-ws--conn)))
22+
23+
(defun open-in-ema ()
24+
(interactive)
25+
(ema-ws-connect)
26+
(when ema-ws--conn
27+
(when-let ((fp (buffer-file-name)))
28+
(websocket-send-text ema-ws--conn fp))))
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# This is test
2+
3+
- [go to index](index)

ema/www/ema-shim.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ function init(reconnecting) {
156156
if (evt.data.startsWith("REDIRECT ")) {
157157
console.log("ema: redirect");
158158
document.location.href = evt.data.slice("REDIRECT ".length);
159+
} else if (evt.data.startsWith("SWITCH ")) {
160+
console.log("ema: switch");
161+
switchRoute(evt.data.slice("SWITCH ".length));
159162
} else {
160163
console.log("ema: ✍ Patching DOM");
161164
setHtml(document.documentElement, evt.data);

0 commit comments

Comments
 (0)