Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: "CI"
on:
pull_request:
push:
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v2
- uses: cachix/install-nix-action@v8
# This also runs nix-build.
- uses: cachix/cachix-action@v6
with:
name: srid
signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}'
# Only needed for private caches
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
# This builds neuron, as well as run tests
- run: nix-build -j4
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Switch to GHC 8.6 (for reflex-dom)
- Raw HTML support (#191)
- Introduce new "uplink tree" view (#195)
- Resilient error handling (#202)
- Bug fixes
- Fix 'neuron new' generating invalid Markdown when title contains special characters (#163)

Expand Down
4 changes: 2 additions & 2 deletions dep/reflex-dom-pandoc/github.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"repo": "reflex-dom-pandoc",
"branch": "master",
"private": false,
"rev": "2fb4d7c9201d39c4a23e8e7067f36474674c22cd",
"sha256": "06l7a2rs7xi14d5nqz873pvl270vdhi863b7axxi6n6ybzzif2rr"
"rev": "9d577e78112f286795e5888e43f2b24ea4aa282a",
"sha256": "0csxpmd3f4bh2iiiark8wfvdzb4gwvg84wm079fnwjwrn3jirpgz"
}
5 changes: 3 additions & 2 deletions neuron.cabal
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cabal-version: 2.4
name: neuron
-- This version must be in sync with what's in Default.dhall
version: 0.5.1.0
version: 0.5.2.0
license: AGPL-3.0-only
copyright: 2020 Sridhar Ratnakumar
maintainer: srid@srid.ca
Expand Down Expand Up @@ -66,6 +66,7 @@ common library-common
reflex-dom-core,
reflex-dom-pandoc,
clay,
tagged

library
import: library-common
Expand All @@ -79,6 +80,7 @@ library
Neuron.Zettelkasten.Connection
Neuron.Zettelkasten.Query
Neuron.Zettelkasten.Query.Error
Neuron.Zettelkasten.Query.Error.Internal
Neuron.Zettelkasten.Query.Eval
Neuron.Zettelkasten.Query.Parser
Neuron.Zettelkasten.Query.Theme
Expand Down Expand Up @@ -156,7 +158,6 @@ common app-common
Neuron.Web.Route
Neuron.Web.View
Neuron.Markdown
Neuron.Zettelkasten.Error
Neuron.Zettelkasten.ID.Scheme
Paths_neuron
Text.Megaparsec.Simple
Expand Down
4 changes: 3 additions & 1 deletion project.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{ system ? builtins.currentSystem }:
{ system ? builtins.currentSystem, withHoogle ? false }:
(import ./dep/reflex-platform { inherit system; }).project ({ pkgs, hackGet, ... }:
let
gitignoreSrc = pkgs.fetchFromGitHub {
Expand All @@ -22,6 +22,8 @@ let
'';

in {
inherit withHoogle;

shellToolOverrides = ghc: super: {
inherit neuronSearchScript;
};
Expand Down
15 changes: 6 additions & 9 deletions src/app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

module Main where

import Clay ((?), Css, em, pct)
import Clay ((?), Css, em)
import qualified Clay as C
import qualified Data.Text as T
import Development.Shake
Expand All @@ -15,7 +15,7 @@ import Neuron.CLI (run)
import Neuron.Config (Config)
import qualified Neuron.Config as Config
import Neuron.Web.Generate (generateSite)
import Neuron.Web.Route (Route (..))
import Neuron.Web.Route (Route (..), RouteError)
import Neuron.Web.View (renderRouteBody, renderRouteHead, style)
import Reflex.Dom.Core
import Reflex.Dom.Pandoc.Document (PandocBuilder)
Expand All @@ -29,14 +29,15 @@ generateMainSite :: Action ()
generateMainSite = do
Rib.buildStaticFiles ["static/**"]
config <- Config.getConfig
let writeHtmlRoute :: Route g a -> (g, a) -> Action ()
let writeHtmlRoute :: Route g a -> (g, a) -> Action (RouteError a)
writeHtmlRoute r x = do
html <- liftIO $ fmap snd $ renderStatic $ renderPage config r x
(errors, html) <- liftIO $ renderStatic $ renderPage config r x
-- FIXME: Make rib take bytestrings
Rib.writeRoute r $ decodeUtf8 @Text html
pure errors
void $ generateSite config writeHtmlRoute

renderPage :: PandocBuilder t m => Config -> Route g a -> (g, a) -> m ()
renderPage :: PandocBuilder t m => Config -> Route g a -> (g, a) -> m (RouteError a)
renderPage config r val = elAttr "html" ("lang" =: "en") $ do
el "head" $ do
renderRouteHead config r val
Expand Down Expand Up @@ -81,12 +82,8 @@ mainStyle cfg = do
C.fontFamily [bodyFont] [C.serif]
C.paddingTop $ em 1
C.paddingBottom $ em 1
"p" ? do
C.lineHeight $ pct 150
"h1, h2, h3, h4, h5, h6, .ui.header, .headerFont" ? do
C.fontFamily [headerFont] [C.sansSerif]
"img" ? do
C.maxWidth $ pct 100 -- Prevents large images from overflowing beyond zettel borders
"code, pre, tt, .monoFont" ? do
C.fontFamily [monoFont, "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New"] [C.monospace]
style cfg
6 changes: 4 additions & 2 deletions src/app/Neuron/CLI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Neuron.CLI.Rib
import Neuron.CLI.Search (interactiveSearch)
import qualified Neuron.Version as Version
import qualified Neuron.Web.Generate as Gen
import qualified Neuron.Zettelkasten.Graph as G
import qualified Neuron.Zettelkasten.Query as Q
import Options.Applicative
import Relude
Expand Down Expand Up @@ -62,7 +63,8 @@ runWith act App {..} =
Query someQ ->
runRibOnceQuietly notesDir $ do
withSome someQ $ \q -> do
result <- flip Q.runQuery q <$> Gen.loadZettels
putLTextLn $ Aeson.encodeToLazyText $ Q.queryResultJson notesDir q result
(graph, errors) <- Gen.loadZettelkasten
let result = Q.runQuery (G.getZettels graph) q
putLTextLn $ Aeson.encodeToLazyText $ Q.queryResultJson notesDir q result errors
Search searchCmd ->
interactiveSearch notesDir searchCmd
2 changes: 1 addition & 1 deletion src/app/Neuron/CLI/New.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import System.Posix.Process
-- As well as print the path to the created file.
newZettelFile :: NewCommand -> Action ()
newZettelFile NewCommand {..} = do
zettels <- Gen.loadZettels
zettels <- Gen.loadZettelsIgnoringErrors
mzid <- withSome idScheme $ \scheme -> do
val <- liftIO $ IDScheme.genVal scheme
pure $ IDScheme.nextAvailableZettelID (Set.fromList $ fmap zettelID zettels) val scheme
Expand Down
5 changes: 3 additions & 2 deletions src/app/Neuron/CLI/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ import Data.Time
import Neuron.Zettelkasten.ID (ZettelID, parseZettelID')
import Neuron.Zettelkasten.ID.Scheme (IDScheme (..))
import Neuron.Zettelkasten.Query as Q
import Neuron.Zettelkasten.Zettel.Meta (zettelDateFormat)
import qualified Neuron.Zettelkasten.Query.Error as Q
import qualified Neuron.Zettelkasten.Query.Parser as Q
import Neuron.Zettelkasten.Zettel.Meta (zettelDateFormat)
import Options.Applicative
import Relude
import qualified Rib.Cli
Expand Down Expand Up @@ -153,7 +154,7 @@ commandParser defaultNotesDir today = do
queryReader =
eitherReader $ \(toText -> s) -> case URI.mkURI s of
Right uri ->
either (Left . show) (maybe (Left "Unsupported query") Right) $ Q.queryFromURI uri
either (Left . toString . Q.showQueryParseError) (maybe (Left "Unsupported query") Right) $ Q.queryFromURI uri
Left e ->
Left $ displayException e
nonEmptyTextReader :: ReadM Text
Expand Down
93 changes: 62 additions & 31 deletions src/app/Neuron/Web/Generate.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,99 +3,130 @@
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE NoImplicitPrelude #-}

-- | Main module for using neuron as a library, instead of as a CLI tool.
module Neuron.Web.Generate
( generateSite,
loadZettels,
loadZettelkasten,
loadZettelsIgnoringErrors,
)
where

import Control.Monad.Except (MonadError, liftEither, runExceptT, withExceptT)
import Control.Monad.Writer (runWriterT)
import qualified Data.Graph.Labelled as G
import qualified Data.Map.Strict as Map
import Data.Traversable
import Development.Shake
import Neuron.Config (Config (..))
import Neuron.Config.Alias (Alias (..), getAliases)
import Neuron.Version (neuronVersion, olderThan)
import qualified Neuron.Web.Route as Z
import Neuron.Zettelkasten.Connection (Connection (..))
import Neuron.Zettelkasten.Error (NeuronError (..))
import qualified Neuron.Zettelkasten.Graph as G
import Neuron.Zettelkasten.Graph.Type (ZettelGraph)
import Neuron.Zettelkasten.ID (mkZettelID)
import Neuron.Zettelkasten.Query.Eval (expandQueries)
import Neuron.Zettelkasten.ID (ZettelID, mkZettelID)
import Neuron.Zettelkasten.Query.Error (QueryParseError, showQueryError)
import Neuron.Zettelkasten.Query.Eval (queryConnections)
import Neuron.Zettelkasten.Zettel (Zettel, ZettelT (..), mkZettelFromMarkdown)
import Options.Applicative
import Reflex.Class (filterLeft, filterRight)
import Relude
import qualified Rib
import Rib.Route
import System.FilePath

-- | Generate the Zettelkasten site
generateSite ::
Config ->
(forall a. Z.Route ZettelGraph a -> (ZettelGraph, a) -> Action ()) ->
(forall a. Z.Route ZettelGraph a -> (ZettelGraph, a) -> Action (Z.RouteError a)) ->
Action ZettelGraph
generateSite config writeHtmlRoute' = do
when (olderThan $ minVersion config)
$ fail
$ toString
$ "Require neuron mininum version " <> minVersion config <> ", but your neuron version is " <> neuronVersion
zettelGraph <- loadZettelkasten
let writeHtmlRoute v r = writeHtmlRoute' r (zettelGraph, v)
(zettelGraph, errors) <- loadZettelkasten
-- NOTE: Right errors are handled further below in individual zettel generation.
let skippedErrors = Map.mapMaybe leftToMaybe errors
writeHtmlRoute :: forall a. a -> Z.Route ZettelGraph a -> Action (Z.RouteError a)
writeHtmlRoute v r = writeHtmlRoute' r (zettelGraph, v)
-- Generate HTML for every zettel
forM_ (G.getZettels zettelGraph) $ \z ->
-- TODO: Should `Zettel` not contain ZettelID?
-- See duplication in `renderZettel`
writeHtmlRoute z $ Z.Route_Zettel (zettelID z)
forM_ (G.getZettels zettelGraph) $ \z -> do
let r = Z.Route_Zettel $ zettelID z
zerrors <- writeHtmlRoute z r
unless (null zerrors) $ do
reportError r Nothing $ showQueryError <$> zerrors
-- Generate the z-index
writeHtmlRoute () Z.Route_ZIndex
writeHtmlRoute errors Z.Route_ZIndex
-- Generate search page
writeHtmlRoute () Z.Route_Search
-- Write alias redirects, unless a zettel with that name exists.
aliases <- getAliases config zettelGraph
forM_ aliases $ \Alias {..} ->
writeHtmlRoute targetZettel (Z.Route_Redirect aliasZettel)
forM_ (Map.toList skippedErrors) $ \(zid, err) -> do
reportError (Z.Route_Zettel zid) (Just "SKIPPED") [err]
pure zettelGraph

loadZettels :: Action [Zettel]
loadZettels =
fmap G.getZettels loadZettelkasten
-- | Report an error in the terminal
reportError :: (MonadIO m, IsRoute r) => r a -> Maybe Text -> [Text] -> m ()
reportError route mErrorKind errors = do
putTextLn $ "E " <> fromMaybe "Unknown route" (fmap toText $ routeFile route) <> maybe "" (\x -> " (" <> x <> ")") mErrorKind
forM_ errors $ \err ->
putText $ " - " <> indentAllButFirstLine 4 err

loadZettelkasten :: Action ZettelGraph
indentAllButFirstLine :: Int -> Text -> Text
indentAllButFirstLine n = unlines . go . lines
where
go [] = []
go [x] = [x]
go (x : xs) =
x : fmap (toText . (take n (repeat ' ') <>) . toString) xs

loadZettelsIgnoringErrors :: Action [Zettel]
loadZettelsIgnoringErrors =
fmap (G.getZettels . fst) loadZettelkasten

loadZettelkasten :: Action (ZettelGraph, Map ZettelID (Either Text [QueryParseError]))
loadZettelkasten =
loadZettelkastenFrom =<< Rib.forEvery ["*.md"] pure

-- | Load the Zettelkasten from disk, using the given list of zettel files
loadZettelkastenFrom :: [FilePath] -> Action ZettelGraph
loadZettelkastenFrom :: [FilePath] -> Action (ZettelGraph, Map ZettelID (Either Text [QueryParseError]))
loadZettelkastenFrom files = do
notesDir <- Rib.ribInputDir
zettels <- forM files $ \((notesDir </>) -> path) -> do
parseRes <- forM files $ \((notesDir </>) -> path) -> do
s <- toText <$> readFile' path
let zid = mkZettelID path
case mkZettelFromMarkdown zid s snd of
Left e -> fail $ toString e
Right zettel -> pure zettel
either (fail . show) pure $ mkZettelGraph zettels
pure $ first (zid,) $ mkZettelFromMarkdown zid s snd
let skippedZettelErrors :: [(ZettelID, Text)] = filterLeft parseRes
(g, errors) <- mkZettelGraph $ filterRight parseRes
pure (g, fmap Left (Map.fromList skippedZettelErrors) `Map.union` fmap Right errors)

-- | Build the Zettelkasten graph from a list of zettels
--
-- Also return the markdown extension to use for each zettel.
mkZettelGraph ::
forall m.
MonadError NeuronError m =>
Monad m =>
[Zettel] ->
m ZettelGraph
m (ZettelGraph, Map ZettelID [QueryParseError])
mkZettelGraph zettels = do
res :: [(Zettel, [(Maybe Connection, Zettel)])] <- liftEither =<< do
flip runReaderT zettels $ runExceptT $ do
for zettels $ \z -> withExceptT (NeuronError_BadQuery (zettelID z)) $ do
runWriterT $ expandQueries z
let g :: ZettelGraph = G.mkGraphFrom (fst <$> res) $ flip concatMap res $ \(z1, conns) ->
res :: [(Zettel, ([(Maybe Connection, Zettel)], [QueryParseError]))] <- do
flip runReaderT zettels $ do
for zettels $ \z -> fmap (z,) $ do
runWriterT $ queryConnections (zettelContent z)
let g :: ZettelGraph = G.mkGraphFrom (fst <$> res) $ flip concatMap res $ \(z1, fst -> conns) ->
conns <&> \(c, z2) -> (connectionMonoid (fromMaybe Folgezettel c), z1, z2)
pure g
pure
( g,
Map.fromList $ flip mapMaybe res $ \(z, (_conns, errs)) ->
if null errs
then Nothing
else Just (zettelID z, errs)
)
where
connectionMonoid = Just
16 changes: 15 additions & 1 deletion src/app/Neuron/Web/Route.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE NoImplicitPrelude #-}

Expand All @@ -18,6 +19,7 @@ import Neuron.Config
import Neuron.Markdown (getFirstParagraphText)
import Neuron.Zettelkasten.Graph.Type
import Neuron.Zettelkasten.ID
import Neuron.Zettelkasten.Query.Error
import Neuron.Zettelkasten.Zettel
import Relude
import Rib (IsRoute (..), routeUrl, routeUrlRel)
Expand All @@ -29,10 +31,22 @@ import qualified Text.URI as URI

data Route graph a where
Route_Redirect :: ZettelID -> Route ZettelGraph ZettelID
Route_ZIndex :: Route ZettelGraph ()
-- ZIndex takes a report of all errors in the zettelkasten.
-- `Left` is skipped zettels; and Right is valid zettels with invalid query links.
Route_ZIndex :: Route ZettelGraph (Map ZettelID (Either Text [QueryParseError]))
Route_Search :: Route ZettelGraph ()
Route_Zettel :: ZettelID -> Route ZettelGraph Zettel

type family RouteError r

type instance RouteError (Map ZettelID (Either Text [QueryParseError])) = ()

type instance RouteError ZettelID = ()

type instance RouteError () = ()

type instance RouteError Zettel = [QueryError]

instance IsRoute (Route graph) where
routeFile = \case
Route_Redirect zid ->
Expand Down
Loading