diff options
author | Dougal <d.maclaurin@gmail.com> | 2024-01-05 15:34:29 -0500 |
---|---|---|
committer | Dougal <d.maclaurin@gmail.com> | 2024-01-05 15:41:54 -0500 |
commit | feba99f2c6d61251922418601e39cd7d7deba786 (patch) | |
tree | 195728317eb2119756cf3033eac1019c9581dd5c | |
parent | a7d5c6f5502346c7e0e806a18240b300c5ea9c0e (diff) |
Get static HTML pages working with the hover-info goodness
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | dex.cabal | 21 | ||||
-rw-r--r-- | makefile | 18 | ||||
-rwxr-xr-x | misc/build-web-index | 2 | ||||
-rw-r--r-- | src/dex.hs | 129 | ||||
-rw-r--r-- | src/lib/Actor.hs | 10 | ||||
-rw-r--r-- | src/lib/Live/Eval.hs | 32 | ||||
-rw-r--r-- | src/lib/Live/Web.hs | 15 | ||||
-rw-r--r-- | src/lib/RenderHtml.hs | 64 | ||||
-rw-r--r-- | src/lib/TopLevel.hs | 60 | ||||
-rw-r--r-- | src/lib/Util.hs | 5 | ||||
-rw-r--r-- | static/dynamic.html | 24 | ||||
-rw-r--r-- | static/index.ts | 10 |
13 files changed, 172 insertions, 220 deletions
@@ -12,7 +12,7 @@ To learn more, check out our our [tutorial](https://google-research.github.io/dex-lang/examples/tutorial.html) or these example programs: - * [Dex prelude](https://google-research.github.io/dex-lang/prelude.html) + * [Dex prelude](https://google-research.github.io/dex-lang/lib/prelude.html) * [Mandelbrot set](https://google-research.github.io/dex-lang/examples/mandelbrot.html) * [Ray tracer](https://google-research.github.io/dex-lang/examples/raytrace.html) * [Estimating pi](https://google-research.github.io/dex-lang/examples/pi.html) @@ -14,9 +14,9 @@ license-file: LICENSE build-type: Simple data-files: lib/*.dx - , static/*.css - , static/*.html - , static/*.js + , static/dynamic.html + , static/index.js + , static/style.css , src/lib/dexrt.bc flag cuda @@ -27,10 +27,6 @@ flag optimized description: Enables GHC optimizations default: False -flag live - description: Enables live-editing environments (web notebook and terminal) - default: True - flag llvm-head description: Use the bleeding-edge version of LLVM default: False @@ -99,8 +95,7 @@ library , QueryTypePure , Util , Vectorize - if flag(live) - exposed-modules: Actor + , Actor , Live.Eval , Live.Web , RenderHtml @@ -136,16 +131,13 @@ library , time -- Floating-point pedanticness (correcting for GHC < 9.2.2) , floating-bits - if flag(live) - build-depends: binary + , binary , blaze-html , blaze-markup , cmark , http-types , wai , warp - cpp-options: -DDEX_LIVE - cxx-options: -DDEX_LIVE if flag(debug) cpp-options: -DDEX_DEBUG default-language: Haskell2010 @@ -252,6 +244,7 @@ executable dex ghc-options: -threaded -optP-Wno-nonportable-include-path -rtsopts + -Wall "-with-rtsopts=-I0 -A16m" default-extensions: CPP , DeriveGeneric @@ -260,8 +253,6 @@ executable dex , BlockArguments if flag(cuda) cpp-options: -DDEX_CUDA - if flag(live) - cpp-options: -DDEX_LIVE if flag(optimized) ghc-options: -O3 else @@ -412,6 +412,7 @@ bench-summary: # --- building docs --- slow-pages = pages/examples/mnist-nearest-neighbors.html +static-names = dynamic.html index.js style.css doc-files = $(doc-names:%=doc/%.dx) pages-doc-files = $(doc-names:%=pages/%.html) @@ -420,30 +421,35 @@ pages-example-files = $(example-names:%=pages/examples/%.html) lib-files = $(filter-out lib/prelude.dx,$(wildcard lib/*.dx)) pages-lib-files = $(patsubst %.dx,pages/%.html,$(lib-files)) +static-files = $(static-names:%=pages/static/%) -docs: pages-prelude $(pages-doc-files) $(pages-example-files) $(pages-lib-files) $(slow-pages) pages/index.md +docs: $(static-files) pages-prelude $(pages-doc-files) $(pages-example-files) $(pages-lib-files) $(slow-pages) pages/index.md + +pages/static/%: static/% + mkdir -p pages/static + cp $^ $@ pages-prelude: lib/prelude.dx - mkdir -p pages - $(dex) --prelude /dev/null script lib/prelude.dx --outfmt html > pages/prelude.html + mkdir -p pages/lib + $(dex) --prelude /dev/null generate-html lib/prelude.dx lib/prelude pages/examples/tutorial.html: tutorial-data pages/examples/dither.html: dither-data pages/examples/%.html: examples/%.dx mkdir -p pages/examples - $(dex) script $< --outfmt html > $@ + $(dex) generate-html $< examples/$* pages/lib/%.html: lib/%.dx mkdir -p pages/lib - $(dex) script $^ --outfmt html > $@ + $(dex) generate-html $^ lib/$* pages/index.md: $(doc-files) $(example-files) $(lib-files) python3 misc/build-web-index "$(doc-files)" "$(example-files)" "$(lib-files)" > $@ ${pages-doc-files}:pages/%.html: doc/%.dx mkdir -p pages - $(dex) script $^ --outfmt html > $@ + $(dex) generate-html $^ $* clean: $(STACK) clean diff --git a/misc/build-web-index b/misc/build-web-index index 88a571c5..75c6de1a 100755 --- a/misc/build-web-index +++ b/misc/build-web-index @@ -49,7 +49,7 @@ def main(): print(""); print("## Libraries"); print("") - print("- [lib/prelude.dx](prelude.html): The Dex Prelude (automatically imported)") + print("- [lib/prelude.dx](lib/prelude.html): The Dex Prelude (automatically imported)") file_block(libraries.split()) @@ -4,8 +4,6 @@ -- license that can be found in the LICENSE file or at -- https://developers.google.com/open-source/licenses/bsd -{-# LANGUAGE RecordWildCards #-} - import System.Console.Haskeline import System.Exit import Control.Monad @@ -14,88 +12,76 @@ import Options.Applicative hiding (Success, Failure) import Text.PrettyPrint.ANSI.Leijen (text, hardline) import System.Posix.Terminal (queryTerminal) import System.Posix.IO (stdOutput) -import System.IO (openFile, IOMode (..)) -import qualified Data.ByteString as BS import Data.List import qualified Data.Text as T -import qualified Data.Text.Encoding as T import qualified Data.Map.Strict as M import qualified System.Console.ANSI as ANSI import System.Console.ANSI hiding (Color) import TopLevel -import Err -import Name import AbstractSyntax (parseTopDeclRepl) import ConcreteSyntax (keyWordStrs, preludeImportBlock) -import RenderHtml --- import Live.Terminal (runTerminal) -import Live.Web (runWeb) +import Live.Web import PPrint hiding (hardline) import Core -import Types.Core import Types.Imp import Types.Source import Types.Top import MonadUtil +import Util (readFileText) data DocFmt = ResultOnly | TextDoc - | JSONDoc -#ifdef DEX_LIVE - | HTMLDoc -#endif -data EvalMode = ReplMode String - | ScriptMode FilePath DocFmt ErrorHandling - | ExportMode FilePath FilePath -- Dex path, .o path - | ClearCache -#ifdef DEX_LIVE + +data EvalMode = ReplMode + | ScriptMode FilePath DocFmt | WebMode FilePath - | WatchMode FilePath -#endif + | GenerateHTML FilePath FilePath + | ClearCache data CmdOpts = CmdOpts EvalMode EvalConfig -runMode :: EvalMode -> EvalConfig -> IO () -runMode evalMode cfg = case evalMode of - ScriptMode fname fmt onErr -> do +runMode :: CmdOpts -> IO () +runMode (CmdOpts evalMode cfg) = case evalMode of + ScriptMode fname fmt -> do env <- loadCache - ((), finalEnv) <- runTopperM cfg env do - source <- liftIO $ T.decodeUtf8 <$> BS.readFile fname - evalSourceText source $ printIncrementalSource fmt + ((), finalEnv) <- runTopperM cfg stdOutLogger env do + blocks <- parseSourceBlocks <$> readFileText fname + forM_ blocks \block -> do + case fmt of + ResultOnly -> return () + TextDoc -> liftIO $ putStr $ pprint block + evalSourceBlockRepl block storeCache finalEnv - ReplMode prompt -> do + ReplMode -> do env <- loadCache - void $ runTopperM cfg env do + void $ runTopperM cfg stdOutLogger env do void $ evalSourceBlockRepl preludeImportBlock forever do - block <- readSourceBlock prompt + block <- readSourceBlock void $ evalSourceBlockRepl block - ClearCache -> clearCache -#ifdef DEX_LIVE WebMode fname -> do env <- loadCache runWeb fname cfg env - WatchMode _ -> error "not implemented" -#endif - -printIncrementalSource :: DocFmt -> SourceBlock -> IO () -printIncrementalSource fmt sb = case fmt of - ResultOnly -> return () - JSONDoc -> return () - TextDoc -> putStr $ pprint sb -#ifdef DEX_LIVE - HTMLDoc -> return () -#endif - -readSourceBlock :: (MonadIO (m n), EnvReader m) => String -> m n SourceBlock -readSourceBlock prompt = do + GenerateHTML fname dest -> do + env <- loadCache + generateHTML fname dest cfg env + ClearCache -> clearCache + +stdOutLogger :: Outputs -> IO () +stdOutLogger (Outputs outs) = do + isatty <- queryTerminal stdOutput + forM_ outs \out -> putStr $ printOutput isatty out + +readSourceBlock :: (MonadIO (m n), EnvReader m) => m n SourceBlock +readSourceBlock = do sourceMap <- withEnv $ envSourceMap . moduleEnv let filenameAndDexCompletions = completeQuotedWord (Just '\\') "\"'" listFiles (dexCompletions sourceMap) let hasklineSettings = setComplete filenameAndDexCompletions defaultSettings liftIO $ runInputT hasklineSettings $ readMultiline prompt (parseTopDeclRepl . T.pack) + where prompt = ">=> " dexCompletions :: Monad m => SourceMap n -> CompletionFunc m dexCompletions sourceMap (line, _) = do @@ -133,31 +119,18 @@ helpOption optionName options = parseMode :: Parser EvalMode parseMode = subparser $ - command "repl" (simpleInfo - (ReplMode <$> strOption (long "prompt" <> value ">=> " - <> metavar "STRING" <> help "REPL prompt"))) -#ifdef DEX_LIVE + command "repl" (simpleInfo (pure ReplMode)) <> command "web" (simpleInfo (WebMode <$> sourceFileInfo)) - <> command "watch" (simpleInfo (WatchMode <$> sourceFileInfo)) -#endif + <> command "generate-html" (simpleInfo (GenerateHTML <$> sourceFileInfo <*> destFileInfo)) <> command "clean" (simpleInfo (pure ClearCache)) - <> command "export" (simpleInfo (ExportMode <$> sourceFileInfo <*> objectFileInfo)) - <> command "script" (simpleInfo (ScriptMode <$> sourceFileInfo - <*> option + <> command "script" (simpleInfo (ScriptMode <$> sourceFileInfo <*> option (optionList [ ("literate" , TextDoc) - , ("result-only", ResultOnly) -#ifdef DEX_LIVE - , ("html" , HTMLDoc) -#endif - , ("json" , JSONDoc)]) + , ("result-only", ResultOnly)]) (long "outfmt" <> value TextDoc <> - helpOption "Output format" "literate (default) | result-only | html | json") - <*> flag ContinueOnErr HaltOnErr ( - long "stop-on-error" - <> help "Stop program evaluation when an error occurs (type or runtime)"))) + helpOption "Output format" "literate (default) | result-only | html | json"))) where - sourceFileInfo = argument str (metavar "FILE" <> help "Source program") - objectFileInfo = argument str (metavar "OBJFILE" <> help "Output path (.o file)") + sourceFileInfo = argument str (metavar "FILE" <> help "Source program") + destFileInfo = argument str (metavar "OUTFILE" <> help "Output path") optionList :: [(String, a)] -> ReadM a optionList opts = eitherReader \s -> case lookup s opts of @@ -178,11 +151,7 @@ parseEvalOpts = EvalConfig <*> optional (strOption $ long "prelude" <> metavar "FILE" <> help "Prelude file") <*> flag NoOptimize Optimize (short 'O' <> help "Optimize generated code") <*> enumOption "print" "Print backend" PrintCodegen printBackends - <*> flag ContinueOnErr HaltOnErr ( - long "stop-on-error" - <> help "Stop program evaluation when an error occurs (type or runtime)") <*> enumOption "loglevel" "Log level" NormalLogLevel logLevels - <*> pure stdOutLogger where printBackends = [ ("haskell", PrintHaskell) , ("dex" , PrintCodegen) ] @@ -191,21 +160,16 @@ parseEvalOpts = EvalConfig logLevels = [ ("normal", NormalLogLevel) , ("debug" , DebugLogLevel ) ] -stdOutLogger :: Outputs -> IO () -stdOutLogger (Outputs outs) = do - isatty <- queryTerminal stdOutput - forM_ outs \out -> putStr $ printOutput isatty out - printOutput :: Bool -> Output -> String printOutput isatty out = case out of Error _ -> addColor isatty Red $ addPrefix ">" $ pprint out _ -> addPrefix (addColor isatty Cyan ">") $ pprint $ out addPrefix :: String -> String -> String -addPrefix prefix str = unlines $ map prefixLine $ lines str +addPrefix prefix s = unlines $ map prefixLine $ lines s where prefixLine :: String -> String - prefixLine s = case s of "" -> prefix - _ -> prefix ++ " " ++ s + prefixLine l = case l of "" -> prefix + _ -> prefix ++ " " ++ l addColor :: Bool -> ANSI.Color -> String -> String addColor False _ s = s @@ -213,14 +177,13 @@ addColor True c s = setSGRCode [SetConsoleIntensity BoldIntensity, SetColor Foreground Vivid c] ++ s ++ setSGRCode [Reset] - pathOption :: ReadM [LibPath] pathOption = splitPaths [] <$> str where splitPaths :: [LibPath] -> String -> [LibPath] splitPaths revAcc = \case [] -> reverse revAcc - str -> let (p,t) = break (==':') str in + s -> let (p,t) = break (==':') s in splitPaths (parseLibPath p:revAcc) (dropWhile (==':') t) parseLibPath = \case @@ -228,6 +191,4 @@ pathOption = splitPaths [] <$> str path -> LibDirectory path main :: IO () -main = do - CmdOpts evalMode opts <- execParser parseOpts - runMode evalMode opts +main = execParser parseOpts >>= runMode diff --git a/src/lib/Actor.hs b/src/lib/Actor.hs index c722ebdd..6b935f5f 100644 --- a/src/lib/Actor.hs +++ b/src/lib/Actor.hs @@ -17,15 +17,14 @@ import Control.Concurrent import Control.Monad import Control.Monad.State.Strict import Control.Monad.Reader -import qualified Data.ByteString as BS import Data.IORef -import Data.Text.Encoding qualified as T import Data.Text (Text) import System.Directory (getModificationTime) import GHC.Generics import IncState import MonadUtil +import Util (readFileText) -- === Actor implementation === @@ -190,9 +189,6 @@ launchClock intervalMicroseconds mailbox = type SourceFileContents = Text type FileWatcher = StateServer (Overwritable SourceFileContents) -readFileContents :: MonadIO m => FilePath -> m Text -readFileContents path = liftIO $ T.decodeUtf8 <$> BS.readFile path - data FileWatcherMsg = ClockSignal_FW () | Subscribe_FW (SubscribeMsg (Overwritable Text)) @@ -203,7 +199,7 @@ launchFileWatcher path = sliceMailbox Subscribe_FW <$> launchActor (fileWatcherI fileWatcherImpl :: FilePath -> ActorM FileWatcherMsg () fileWatcherImpl path = do - initContents <- readFileContents path + initContents <- readFileText path t0 <- liftIO $ getModificationTime path launchClock 100000 =<< selfMailbox ClockSignal_FW modTimeRef <- newRef t0 @@ -213,7 +209,7 @@ fileWatcherImpl path = do tOld <- readRef modTimeRef tNew <- liftIO $ getModificationTime path when (tNew /= tOld) do - newContents <- readFileContents path + newContents <- readFileText path update $ OverwriteWith newContents flushDiffs writeRef modTimeRef tNew diff --git a/src/lib/Live/Eval.hs b/src/lib/Live/Eval.hs index 3b1ba69d..7af2c58d 100644 --- a/src/lib/Live/Eval.hs +++ b/src/lib/Live/Eval.hs @@ -9,7 +9,7 @@ module Live.Eval ( watchAndEvalFile, EvalServer, CellState (..), CellUpdate (..), CellsState, CellsUpdate, NodeList (..), NodeListUpdate (..), subscribeIO, - CellStatus (..), nodeListAsUpdate, NodeId) where + CellStatus (..), nodeListAsUpdate, NodeId, evalFileNonInteractive) where import Control.Concurrent import Control.Monad @@ -20,6 +20,7 @@ import Data.Aeson (ToJSON) import Data.Functor ((<&>)) import Data.Maybe (fromJust) import Data.Text (Text) +import Data.IORef import Prelude hiding (span) import GHC.Generics @@ -29,6 +30,7 @@ import Types.Source import TopLevel import ConcreteSyntax import MonadUtil +import Util (readFileText) -- === Top-level interface === @@ -43,9 +45,22 @@ watchAndEvalFile fname opts env = do launchDagEvaluator opts parser env sourceBlockEvalFun :: EvalConfig -> Mailbox Outputs -> TopStateEx -> SourceBlock -> IO (ExitStatus, TopStateEx) -sourceBlockEvalFun cfg resultChan env block = do - let cfg' = cfg { cfgLogAction = send resultChan } - evalSourceBlockIO cfg' env block +sourceBlockEvalFun cfg resultChan env block = + evalSourceBlockIO cfg (send resultChan) env block + +-- === Evaluating non-interactively to produce a standalone HTML page === + +evalFileNonInteractive :: FilePath -> EvalConfig -> TopStateEx -> IO CellsState +evalFileNonInteractive fname cfg initEnv = do + envRef <- newIORef initEnv + blocks <- parseSourceBlocks <$> readFileText fname + cellStates <- forM blocks \block -> do + env <- readIORef envRef + ((exitStatus, newEnv), outs) <- captureLogs \logger -> + evalSourceBlockIO cfg logger env block + writeIORef envRef newEnv + return $ CellState block (exitStatusAsCellStatus exitStatus) outs + runFreshNameT $ buildNodeList cellStates -- === DAG diff state === @@ -253,9 +268,7 @@ processJobUpdate jobId jobUpdate = do let nodeId = snd jobId case jobUpdate of JobComplete (exitStatus, newEnv) -> do - let newStatus = case exitStatus of - ExitSuccess -> Complete - ExitFailure -> CompleteWithErrors + let newStatus = exitStatusAsCellStatus exitStatus update $ UpdateCellState nodeId $ CellUpdate (OverwriteWith newStatus) mempty update $ UpdateCurJob Nothing update $ AppendEnv newEnv @@ -264,6 +277,11 @@ processJobUpdate jobId jobUpdate = do PartialJobUpdate result -> update $ UpdateCellState nodeId $ CellUpdate NoChange result Nothing -> return () -- this job is a zombie +exitStatusAsCellStatus :: ExitStatus -> CellStatus +exitStatusAsCellStatus = \case + ExitSuccess -> Complete + ExitFailure -> CompleteWithErrors + nextCellIndex :: EvaluatorM Int nextCellIndex = do envs <- getl PrevEnvs diff --git a/src/lib/Live/Web.hs b/src/lib/Live/Web.hs index dfac48bc..25a62050 100644 --- a/src/lib/Live/Web.hs +++ b/src/lib/Live/Web.hs @@ -4,7 +4,7 @@ -- license that can be found in the LICENSE file or at -- https://developers.google.com/open-source/licenses/bsd -module Live.Web (runWeb) where +module Live.Web (runWeb, generateHTML) where import Control.Concurrent (readChan) import Control.Monad (forever) @@ -17,6 +17,7 @@ import Data.Aeson (ToJSON, encode) import Data.Binary.Builder (fromByteString) import Data.ByteString.Lazy (toStrict) import qualified Data.ByteString as BS +import System.Directory (withCurrentDirectory) -- import Paths_dex (getDataFileName) import RenderHtml @@ -29,6 +30,16 @@ runWeb fname opts env = do putStrLn "Streaming output to http://localhost:8000/" run 8000 $ serveResults resultsChan +pagesDir :: FilePath +pagesDir = "pages" + +generateHTML :: FilePath -> FilePath -> EvalConfig -> TopStateEx -> IO () +generateHTML sourcePath destPath cfg env = do + finalState <- evalFileNonInteractive sourcePath cfg env + results <- renderResults finalState + withCurrentDirectory pagesDir do + renderStandaloneHTML destPath results + serveResults :: EvalServer -> Application serveResults resultsSubscribe request respond = do print (pathInfo request) @@ -52,7 +63,7 @@ resultStream :: EvalServer -> StreamingBody resultStream resultsServer write flush = do sendUpdate ("start"::String) (initResult, resultsChan) <- subscribeIO resultsServer - (renderedInit, renderUpdateFun) <- renderResults initResult + (renderedInit, renderUpdateFun) <- renderResultsInc initResult sendUpdate renderedInit forever $ readChan resultsChan >>= renderUpdateFun >>= sendUpdate where diff --git a/src/lib/RenderHtml.hs b/src/lib/RenderHtml.hs index fe4d7703..d1d884ea 100644 --- a/src/lib/RenderHtml.hs +++ b/src/lib/RenderHtml.hs @@ -7,15 +7,15 @@ {-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-incomplete-patterns #-} -module RenderHtml ( - progHtml, pprintHtml, ToMarkup, renderSourceBlock, renderResults, - RenderedSourceBlock, RenderedOutputs) where +module RenderHtml (renderResults, renderResultsInc, renderStandaloneHTML) where import Text.Blaze.Internal (MarkupM) import Text.Blaze.Html5 as H hiding (map, a, b) import Text.Blaze.Html5.Attributes as At import Text.Blaze.Html.Renderer.String -import Data.Aeson (ToJSON (..)) +import Data.ByteString.Lazy (toStrict) +import qualified Data.ByteString as BS +import Data.Aeson (ToJSON (..), encode) import qualified Data.Map.Strict as M import Control.Monad.State.Strict import Data.Foldable (fold) @@ -23,13 +23,10 @@ import Data.Maybe (fromJust) import Data.Functor ((<&>)) import Data.String (fromString) import Data.Text qualified as T -import Data.Text.IO qualified as T import CMark (commonmarkToHtml) -import System.IO.Unsafe import GHC.Generics import Err -import Paths_dex (getDataFileName) import PPrint import Types.Source import Util (unsnoc, foldMapM) @@ -59,8 +56,11 @@ instance IncState RenderedCellState where applyDiff (RenderedCellState sb status result) (RenderedCellUpdate status' result') = RenderedCellState sb (fromOverwritable (applyDiff (Overwritable status) status')) (result <> result') -renderResults :: CellsState -> IO (RenderingUpdate, CellsUpdate -> IO RenderingUpdate) -renderResults initState = do +renderResults :: CellsState -> IO RenderingUpdate +renderResults cellsState = fst <$> renderResultsInc cellsState + +renderResultsInc :: CellsState -> IO (RenderingUpdate, CellsUpdate -> IO RenderingUpdate) +renderResultsInc initState = do (initRender, updates) <- runIncM renderCells initState return (nodeListAsUpdate initRender, updates) @@ -265,38 +265,26 @@ instance ToJSON HighlightType -- ----------------- -cssSource :: T.Text -cssSource = unsafePerformIO $ - T.readFile =<< getDataFileName "static/style.css" -{-# NOINLINE cssSource #-} - -javascriptSource :: T.Text -javascriptSource = unsafePerformIO $ - T.readFile =<< getDataFileName "static/index.js" -{-# NOINLINE javascriptSource #-} - -pprintHtml :: ToMarkup a => a -> String -pprintHtml x = renderHtml $ toMarkup x - -progHtml :: (ToMarkup a, ToMarkup b) => [(a, b)] -> String -progHtml blocks = renderHtml $ wrapBody $ map toHtmlBlock blocks - where toHtmlBlock (block,outputs) = toMarkup block <> toMarkup outputs +renderStandaloneHTML :: FilePath -> RenderingUpdate -> IO () +renderStandaloneHTML pagePath renderingInfo = do + let jsonPath = pagePath ++ ".json" + let htmlPath = pagePath ++ ".html" + BS.writeFile jsonPath $ toStrict $ encode renderingInfo + writeFile htmlPath $ renderHtml $ buildMainHtml jsonPath -wrapBody :: [Html] -> Html -wrapBody blocks = docTypeHtml $ do - H.head $ do +buildMainHtml :: FilePath -> Html +buildMainHtml jsonPath = docTypeHtml $ do + H.head do H.meta ! charset "UTF-8" - -- Base CSS stylesheet. - H.style ! type_ "text/css" $ toHtml cssSource - -- KaTeX CSS and JavaScript. - H.link ! rel "stylesheet" ! href "https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css" - H.script ! defer "" ! src "https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.js" $ mempty - H.script ! defer "" ! src "https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/contrib/auto-render.min.js" - ! onload jsSource $ mempty - H.body $ H.div inner ! At.id "main-output" + H.link ! rel "stylesheet" ! type_ "text/css" ! href "/static/style.css" + H.body ! onload (textValue $ fromString jsSource) $ do + H.div mempty ! At.id "minimap" + H.div "(hover over code for more information)" ! At.id "hover-info" + H.div mempty ! At.id "main-output" + H.script ! src "/static/index.js" $ mempty where - inner = foldMap (cdiv "cell") blocks - jsSource = textValue $ javascriptSource <> "render(RENDER_MODE.STATIC);" + jsSource :: String + jsSource = "render('Static', '/" ++ jsonPath ++ "');" mdToHtml :: T.Text -> Html mdToHtml s = preEscapedText $ commonmarkToHtml [] s diff --git a/src/lib/TopLevel.hs b/src/lib/TopLevel.hs index 777b7051..aa0f94bd 100644 --- a/src/lib/TopLevel.hs +++ b/src/lib/TopLevel.hs @@ -8,13 +8,12 @@ module TopLevel ( EvalConfig (..), Topper, TopperM, runTopperM, - evalSourceBlockRepl, OptLevel (..), - evalSourceText, TopStateEx (..), LibPath (..), + evalSourceBlockRepl, OptLevel (..), TopStateEx (..), LibPath (..), evalSourceBlockIO, initTopState, loadCache, storeCache, clearCache, ensureModuleLoaded, importModule, printCodegen, loadObject, toCFunction, packageLLVMCallable, - simpOptimizations, loweredOptimizations, compileTopLevelFun, ErrorHandling (..), - ExitStatus (..)) where + simpOptimizations, loweredOptimizations, compileTopLevelFun, + ExitStatus (..), parseSourceBlocks, captureLogs) where import Data.Functor import Data.Maybe (catMaybes) @@ -23,7 +22,8 @@ import Control.Monad.Writer.Strict hiding (pass) import Control.Monad.State.Strict import Control.Monad.Reader import qualified Data.ByteString as BS -import Data.Text (Text) +import qualified Data.Text as T +import Data.IORef import Data.Text.Prettyprint.Doc import Data.Store (encode, decode) import Data.String (fromString) @@ -82,7 +82,6 @@ import Vectorize -- === top-level monad === data LibPath = LibDirectory FilePath | LibBuiltinPath -data ErrorHandling = HaltOnErr | ContinueOnErr data EvalConfig = EvalConfig { backendName :: Backend @@ -90,10 +89,9 @@ data EvalConfig = EvalConfig , preludeFile :: Maybe FilePath , optLevel :: OptLevel , printBackend :: PrintBackend - , errorHandling :: ErrorHandling - , cfgLogLevel :: LogLevel - , cfgLogAction :: Outputs -> IO ()} + , cfgLogLevel :: LogLevel } +type LogAction = Outputs -> IO () class Monad m => ConfigReader m where getConfig :: m EvalConfig @@ -114,6 +112,7 @@ class ( forall n. Fallible (m n) data TopperReaderData = TopperReaderData { topperEvalConfig :: EvalConfig + , topperLogAction :: LogAction , topperRuntimeEnv :: RuntimeEnv } newtype TopperM (n::S) a = TopperM @@ -132,12 +131,12 @@ data TopSerializedStateEx where TopSerializedStateEx :: Distinct n => SerializedEnv n -> TopSerializedStateEx runTopperM - :: EvalConfig -> TopStateEx + :: EvalConfig -> LogAction -> TopStateEx -> (forall n. Mut n => TopperM n a) -> IO (a, TopStateEx) -runTopperM opts (TopStateEx env rtEnv) cont = do +runTopperM opts logger (TopStateEx env rtEnv) cont = do Abs frag (LiftE result) <- - flip runReaderT (TopperReaderData opts rtEnv) $ + flip runReaderT (TopperReaderData opts logger rtEnv) $ runTopBuilderT env $ runTopperM' do localTopBuilder $ LiftE <$> cont return (result, extendTopEnv env rtEnv frag) @@ -157,31 +156,22 @@ allocateDynamicVarKeyPtrs = do ptr <- createTLSKey return [(OutStreamDyvar, castPtr ptr)] +captureLogs :: (LogAction -> IO a) -> IO (a, Outputs) +captureLogs cont = do + ref <- newIORef mempty + ans <- cont \outs -> modifyIORef ref (<>outs) + finalOuts <- readIORef ref + return (ans, finalOuts) + -- ====== +parseSourceBlocks :: T.Text -> [SourceBlock] +parseSourceBlocks source = uModuleSourceBlocks $ parseUModule Main source + evalSourceBlockIO - :: EvalConfig -> TopStateEx -> SourceBlock -> IO (ExitStatus, TopStateEx) -evalSourceBlockIO opts env block = - runTopperM opts env $ evalSourceBlockRepl block - --- Used for the top-level source file (rather than imported modules) -evalSourceText :: (Topper m, Mut n) => Text -> (SourceBlock -> IO ()) -> m n () -evalSourceText source logSourceBlock = do - let UModule mname deps sbs = parseUModule Main source - mapM_ ensureModuleLoaded deps - evalSourceBlocks mname sbs - where - evalSourceBlocks mname = \case - [] -> return () - sb:rest -> do - liftIO $ logSourceBlock sb - evalSourceBlock mname sb >>= \case - Success () -> return () - Failure e -> do - logTop $ Error e - (errorHandling <$> getConfig) >>= \case - HaltOnErr -> return () - ContinueOnErr -> evalSourceBlocks mname rest + :: EvalConfig -> LogAction -> TopStateEx -> SourceBlock -> IO (ExitStatus, TopStateEx) +evalSourceBlockIO opts logger env block = + runTopperM opts logger env $ evalSourceBlockRepl block data ExitStatus = ExitSuccess | ExitFailure deriving (Show) @@ -820,7 +810,7 @@ instance Logger Outputs (TopperM n) where getLogLevel = cfgLogLevel <$> getConfig instance HasIOLogger Outputs (TopperM n) where - getIOLogAction = cfgLogAction <$> getConfig + getIOLogAction = TopperM $ asks topperLogAction instance Generic TopStateEx where type Rep TopStateEx = Rep (Env UnsafeS, RuntimeEnv) diff --git a/src/lib/Util.hs b/src/lib/Util.hs index 4dbc43ed..4d7e7241 100644 --- a/src/lib/Util.hs +++ b/src/lib/Util.hs @@ -23,6 +23,8 @@ import Data.Maybe (catMaybes, mapMaybe) import Data.List (sort) import Data.Hashable (Hashable) import Data.Store (Store) +import qualified Data.Text as T +import qualified Data.Text.Encoding as T import qualified Data.List.NonEmpty as NE import qualified Data.ByteString as BS import Data.Foldable @@ -360,6 +362,9 @@ instance Pretty a => Pretty (Tree a) where Leaf x -> pretty x Branch xs -> pretty xs +readFileText :: MonadIO m => FilePath -> m T.Text +readFileText fname = liftIO $ T.decodeUtf8 <$> BS.readFile fname + -- === bytestrings paired with their hash digest === -- TODO: use something other than a string to store the digest diff --git a/static/dynamic.html b/static/dynamic.html index cb4da817..d3dc9891 100644 --- a/static/dynamic.html +++ b/static/dynamic.html @@ -4,29 +4,11 @@ <meta charset="UTF-8"> <title>Dex Output</title> <link rel="stylesheet" href="/style.css" type="text/css" /> - <!-- KaTeX: LaTeX rendering --> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css" integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous"> - <script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.js" integrity="sha384-g7c+Jr9ZivxKLnZTDUhnkOnsh30B4H0rpLUpJ4jAIKs4fnJI+sEnkvrMWph2EDg4" crossorigin="anonymous"></script> - <!-- Do dynamic webpage rendering on-load. --> - <!-- The `onerror` attribute renders Dex output dynamically even if - KaTeX fails to load (e.g., if running dex web locally with no - network); the latex will just remain unrendered. --> - <script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/contrib/auto-render.min.js" - integrity="sha384-mll67QQFJfxn0IYznZYonOWZ644AWYC+Pt2cHqMaRhXVrursRwvLnLaebdGIlYNa" - onload="render('Dynamic');" - onerror="render('Dynamic');" - crossorigin="anonymous"></script> - <!-- jQuery --> - <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script> </head> - -<body> +<body onload="render('Dynamic');"> <div id="minimap"> </div> - <div id="hover-info"> (hover over code for more information) </div> + <div id="hover-info">(hover over code for more information)</div> <main id="main-output"></main> - -<script src="/index.js""></script> - + <script src="/index.js"></script> </body> - </html> diff --git a/static/index.ts b/static/index.ts index 9d712f37..adf19ee6 100644 --- a/static/index.ts +++ b/static/index.ts @@ -343,11 +343,15 @@ function highlightTreeNode(isTemporary: boolean, node: TreeNode, highlightType:H }})} } type RenderMode = "Static" | "Dynamic" -function render(renderMode:RenderMode) { +function render(renderMode:RenderMode, jsonData:string) { switch (renderMode) { case "Static": - // For static pages, simply call rendering functions once. - // renderLaTeX(document); + const req = new XMLHttpRequest() + req.open('GET', jsonData, true) + req.responseType = 'json' + req.onload = function() { + processUpdates(req.response)} + req.send() break case "Dynamic": const source = new EventSource("/getnext"); |