diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c8a2a5bb..633a61c5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - `neuron search` - Revert #429 for neuron-search regression - Deal with title IDs in search (#445) +- Web interface + - Simplify error message UX for missing wiki-links (#448) ## 1.0.1.0 diff --git a/neuron/neuron.cabal b/neuron/neuron.cabal index 08efa7107..a213c546c 100644 --- a/neuron/neuron.cabal +++ b/neuron/neuron.cabal @@ -1,7 +1,7 @@ cabal-version: 2.4 name: neuron -- This version must be in sync with what's in Default.dhall -version: 1.0.6.0 +version: 1.0.7.0 license: AGPL-3.0-only copyright: 2020 Sridhar Ratnakumar maintainer: srid@srid.ca diff --git a/neuron/src/app/Neuron/CLI/Types.hs b/neuron/src/app/Neuron/CLI/Types.hs index 026e1fe05..28d10f445 100644 --- a/neuron/src/app/Neuron/CLI/Types.hs +++ b/neuron/src/app/Neuron/CLI/Types.hs @@ -34,7 +34,6 @@ import qualified Neuron.Web.Route as R import qualified Neuron.Zettelkasten.Connection as C import Neuron.Zettelkasten.ID (ZettelID, parseZettelID) import Neuron.Zettelkasten.ID.Scheme (IDScheme (..)) -import qualified Neuron.Zettelkasten.Query.Error as Q import Neuron.Zettelkasten.Query.Graph as Q import qualified Neuron.Zettelkasten.Query.Parser as Q import Neuron.Zettelkasten.Zettel as Q @@ -219,10 +218,8 @@ commandParser defaultNotesDir now = do queryReader = eitherReader $ \(toText -> s) -> case URI.mkURI s of Right uri -> - either - (Left . toString . Q.showQueryParseError) - (maybe (Left "Unsupported query") Right) - $ Q.queryFromURI connDummy uri + maybe (Left "Not a valid query") Right $ + Q.queryFromURI connDummy uri Left e -> Left $ displayException e dateReader :: ReadM DateMayTime diff --git a/neuron/src/app/Neuron/Web/Generate.hs b/neuron/src/app/Neuron/Web/Generate.hs index dd8d7e204..39ee43460 100644 --- a/neuron/src/app/Neuron/Web/Generate.hs +++ b/neuron/src/app/Neuron/Web/Generate.hs @@ -35,7 +35,7 @@ import qualified Neuron.Web.Route as Z import qualified Neuron.Zettelkasten.Graph.Build as G import Neuron.Zettelkasten.Graph.Type (ZettelGraph) import Neuron.Zettelkasten.ID (ZettelID, getZettelID) -import Neuron.Zettelkasten.Query.Error (showQueryError) +import Neuron.Zettelkasten.Query.Error (showQueryResultError) import Neuron.Zettelkasten.Zettel import Options.Applicative import Relude @@ -83,8 +83,8 @@ generateSite config writeHtmlRoute' = do case err of ZettelError_ParseError (untag -> parseErr) -> parseErr :| [] - ZettelError_QueryErrors queryErrs -> - showQueryError <$> queryErrs + ZettelError_QueryResultErrors queryErrs -> + showQueryResultError <$> queryErrs ZettelError_AmbiguousFiles filePaths -> ("Multiple zettels have the same ID: " <> T.intercalate ", " (fmap toText $ toList filePaths)) :| [] diff --git a/neuron/src/lib/Neuron/Web/Query/View.hs b/neuron/src/lib/Neuron/Web/Query/View.hs index ef01e50f9..690ba8aa9 100644 --- a/neuron/src/lib/Neuron/Web/Query/View.hs +++ b/neuron/src/lib/Neuron/Web/Query/View.hs @@ -12,6 +12,7 @@ module Neuron.Web.Query.View ( renderQueryResult, renderZettelLink, renderZettelLinkIDOnly, + renderMissingZettelLink, style, ) where @@ -32,14 +33,15 @@ import Data.TagTree ) import qualified Data.Text as T import Data.Tree (Forest, Tree (Node)) +import Neuron.Reader.Type (ZettelFormat (ZettelFormat_Markdown)) import Neuron.Web.Route ( NeuronWebT, Route (..), neuronRouteLink, ) -import Neuron.Web.Widget (elTime, semanticIcon) +import Neuron.Web.Widget (elNoSnippetSpan, elTime, semanticIcon) import Neuron.Zettelkasten.Connection (Connection (Folgezettel)) -import Neuron.Zettelkasten.ID (ZettelID (zettelIDRaw)) +import Neuron.Zettelkasten.ID (ZettelID (zettelIDRaw), zettelIDSourceFileName) import Neuron.Zettelkasten.Query.Theme (LinkView (..), ZettelsView (..)) import Neuron.Zettelkasten.Zettel ( Zettel, @@ -128,7 +130,7 @@ renderZettelLink :: NeuronWebT t m () renderZettelLink mInner conn (fromMaybe def -> linkView) Zettel {..} = do let connClass = show <$> conn - rawClass = either (const $ Just "raw") (const Nothing) zettelError + rawClass = maybe Nothing (const $ Just "errors") zettelError mextra = case linkView of LinkView_Default -> @@ -148,10 +150,7 @@ renderZettelLink mInner conn (fromMaybe def -> linkView) Zettel {..} = do elAttr "span" ("class" =: "zettel-link" <> withTooltip linkTooltip) $ do let linkInnerHtml = fromMaybe (text zettelTitle) mInner neuronRouteLink (Some $ Route_Zettel zettelID) mempty linkInnerHtml - case conn of - Just Folgezettel -> elNoSnippetSpan mempty $ do - elAttr "sup" ("title" =: "Branching link (folgezettel)") $ text "ᛦ" - _ -> pure mempty + elConnSuffix conn where linkTooltip = -- If there is custom inner text, put zettel title in tooltip. @@ -162,10 +161,6 @@ renderZettelLink mInner conn (fromMaybe def -> linkView) Zettel {..} = do if null zettelTags then Nothing else Just $ "Tags: " <> T.intercalate "; " (unTag <$> zettelTags) - -- Prevent this element from appearing in Google search results - -- https://developers.google.com/search/reference/robots_meta_tag#data-nosnippet-attr - elNoSnippetSpan :: DomBuilder t m => Map Text Text -> NeuronWebT t m a -> NeuronWebT t m a - elNoSnippetSpan attrs = elAttr "span" ("data-nosnippet" =: "" <> attrs) withTooltip :: Maybe Text -> Map Text Text withTooltip = \case Nothing -> mempty @@ -175,6 +170,24 @@ renderZettelLink mInner conn (fromMaybe def -> linkView) Zettel {..} = do <> "data-position" =: "right center" ) +elConnSuffix :: DomBuilder t m => Maybe Connection -> m () +elConnSuffix mconn = + case mconn of + Just Folgezettel -> elNoSnippetSpan mempty $ do + elAttr "sup" ("title" =: "Branching link (folgezettel)") $ text "ᛦ" + _ -> pure mempty + +-- TODO: Eventually refactor this function to reuse what's in renderZettelLink +renderMissingZettelLink :: DomBuilder t m => Maybe Connection -> ZettelID -> m () +renderMissingZettelLink mconn zid = do + let connClass = show <$> mconn + classes :: [Text] = catMaybes $ [Just "zettel-link-container", Just "errors"] <> [connClass] + elClass "span" (T.intercalate " " classes) $ do + let errMsg = "Broken wiki-link (" <> toText (zettelIDSourceFileName zid ZettelFormat_Markdown) <> " does not exist)" + elAttr "span" ("class" =: "zettel-link" <> "title" =: errMsg) $ do + elAttr "a" mempty $ text $ zettelIDRaw zid + elConnSuffix mconn + -- | Like `renderZettelLink` but when we only have ID in hand. renderZettelLinkIDOnly :: DomBuilder t m => ZettelID -> NeuronWebT t m () renderZettelLinkIDOnly zid = @@ -233,7 +246,7 @@ zettelLinkCss = do C.textDecoration C.none "span.zettel-link-container span.extra" ? do C.color C.auto - "span.zettel-link-container.raw" ? do + "span.zettel-link-container.errors" ? do C.border C.solid (C.px 1) C.red "[data-tooltip]:after" ? do C.fontSize $ em 0.7 diff --git a/neuron/src/lib/Neuron/Web/Widget.hs b/neuron/src/lib/Neuron/Web/Widget.hs index 99d85f960..0570a11b7 100644 --- a/neuron/src/lib/Neuron/Web/Widget.hs +++ b/neuron/src/lib/Neuron/Web/Widget.hs @@ -17,6 +17,11 @@ elTime t = do elAttr "time" ("datetime" =: formatDateMayTime t) $ do text $ formatDay $ getDay t +-- | A pre element with scrollbar +elPreOverflowing :: DomBuilder t m => m a -> m a +elPreOverflowing w = + elAttr "pre" ("style" =: "overflow: auto") w + semanticIcon :: DomBuilder t m => Text -> m () semanticIcon name = elClass "i" (name <> " icon") blank @@ -25,3 +30,8 @@ elLinkGoogleFonts fs = let fsEncoded = T.intercalate "|" $ T.replace " " "+" <$> fs fsUrl = "https://fonts.googleapis.com/css?family=" <> fsEncoded <> "&display=swap" in elAttr "link" ("rel" =: "stylesheet" <> "href" =: fsUrl) blank + +-- Prevent this element from appearing in Google search results +-- https://developers.google.com/search/reference/robots_meta_tag#data-nosnippet-attr +elNoSnippetSpan :: DomBuilder t m => Map Text Text -> m a -> m a +elNoSnippetSpan attrs = elAttr "span" ("data-nosnippet" =: "" <> attrs) \ No newline at end of file diff --git a/neuron/src/lib/Neuron/Web/ZIndex.hs b/neuron/src/lib/Neuron/Web/ZIndex.hs index bddf887af..9a7c6a396 100644 --- a/neuron/src/lib/Neuron/Web/ZIndex.hs +++ b/neuron/src/lib/Neuron/Web/ZIndex.hs @@ -23,13 +23,19 @@ import Data.Tree import qualified Neuron.Web.Query.View as QueryView import Neuron.Web.Route import qualified Neuron.Web.Theme as Theme +import Neuron.Web.Widget (elPreOverflowing) +import Neuron.Web.Zettel.View (renderZettelParseError) import Neuron.Zettelkasten.Connection import Neuron.Zettelkasten.Graph (ZettelGraph) import qualified Neuron.Zettelkasten.Graph as G import Neuron.Zettelkasten.ID (ZettelID (..)) import Neuron.Zettelkasten.Query (zettelsByTag) -import Neuron.Zettelkasten.Query.Error (showQueryError) +import Neuron.Zettelkasten.Query.Error (showQueryResultError) import Neuron.Zettelkasten.Zettel + ( Zettel, + ZettelError (..), + ZettelT (zettelTitle), + ) import Reflex.Dom.Core hiding (mapMaybe, (&)) import Relude hiding ((&)) @@ -123,17 +129,17 @@ renderErrors :: DomBuilder t m => Map ZettelID ZettelError -> NeuronWebT t m () renderErrors errors = do let severity = \case ZettelError_ParseError _ -> "negative" - ZettelError_QueryErrors _ -> "warning" + ZettelError_QueryResultErrors _ -> "warning" ZettelError_AmbiguousFiles _ -> "negative" errorMessageHeader zid = \case ZettelError_ParseError _ -> do text "Zettel " QueryView.renderZettelLinkIDOnly zid text " failed to parse" - ZettelError_QueryErrors _ -> do + ZettelError_QueryResultErrors _ -> do text "Zettel " QueryView.renderZettelLinkIDOnly zid - text " has malformed queries" + text " has broken wiki-links" ZettelError_AmbiguousFiles _ -> do text $ "More than one file define the same zettel ID slug (" @@ -145,11 +151,11 @@ renderErrors errors = do el "p" $ do case zError of ZettelError_ParseError parseError -> - el "pre" $ text $ show parseError - ZettelError_QueryErrors queryErrors -> + renderZettelParseError parseError + ZettelError_QueryResultErrors queryErrors -> el "ol" $ do forM_ queryErrors $ \qe -> - el "li" $ el "pre" $ text $ showQueryError qe + el "li" $ elPreOverflowing $ text $ showQueryResultError qe ZettelError_AmbiguousFiles filePaths -> el "ul" $ do forM_ filePaths $ \fp -> diff --git a/neuron/src/lib/Neuron/Web/Zettel/View.hs b/neuron/src/lib/Neuron/Web/Zettel/View.hs index 7538d33ba..04aa80d01 100644 --- a/neuron/src/lib/Neuron/Web/Zettel/View.hs +++ b/neuron/src/lib/Neuron/Web/Zettel/View.hs @@ -5,6 +5,7 @@ {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE ViewPatterns #-} {-# LANGUAGE NoImplicitPrelude #-} @@ -12,11 +13,14 @@ module Neuron.Web.Zettel.View ( renderZettel, renderZettelContentCard, + renderZettelParseError, ) where import Data.Some import Data.TagTree +import Data.Tagged (untag) +import Neuron.Reader.Type (ZettelParseError) import qualified Neuron.Web.Query.View as Q import Neuron.Web.Route import Neuron.Web.Widget @@ -25,7 +29,7 @@ import qualified Neuron.Web.Widget.InvertedTree as IT import Neuron.Zettelkasten.Connection import Neuron.Zettelkasten.Graph (ZettelGraph) import qualified Neuron.Zettelkasten.Graph as G -import Neuron.Zettelkasten.Query.Error (QueryError, showQueryError) +import Neuron.Zettelkasten.Query.Error (QueryResultError (..)) import qualified Neuron.Zettelkasten.Query.Eval as Q import Neuron.Zettelkasten.Zettel import Reflex.Dom.Core hiding ((&)) @@ -89,24 +93,20 @@ renderZettelBottomPane graph z@Zettel {..} = do evalAndRenderZettelQuery :: PandocBuilder t m => ZettelGraph -> - NeuronWebT t m [QueryError] -> + NeuronWebT t m [QueryResultError] -> URILink -> - NeuronWebT t m [QueryError] + NeuronWebT t m [QueryResultError] evalAndRenderZettelQuery graph oldRender uriLink@(URILink inner _uri) = do case flip runReaderT (G.getZettels graph) (Q.runQueryURILink uriLink) of - Left e -> do - -- Error parsing or running the query. - fmap (e :) oldRender <* elInlineError e + Left e@(QueryResultError_NoSuchZettel mconn zid) -> do + Q.renderMissingZettelLink mconn zid + pure [e] Right Nothing -> do -- This is not a query link; pass through. oldRender Right (Just res) -> do Q.renderQueryResult inner res pure mempty - where - elInlineError e = - elClass "span" "ui left pointing red basic label" $ do - text $ showQueryError e renderZettelContent :: forall t m a. @@ -128,9 +128,13 @@ renderZettelRawContent :: (DomBuilder t m) => ZettelT Text -> m () renderZettelRawContent Zettel {..} = do divClass "ui error message" $ do elClass "h2" "header" $ text "Zettel failed to parse" - el "p" $ el "pre" $ text $ show zettelError + maybe blank renderZettelParseError zettelError elClass "article" "ui raised attached segment zettel-content raw" $ do - el "pre" $ text $ zettelContent + elPreOverflowing $ text $ zettelContent + +renderZettelParseError :: DomBuilder t m => ZettelParseError -> m () +renderZettelParseError err = + el "p" $ elPreOverflowing $ text $ untag err renderTags :: DomBuilder t m => NonEmpty Tag -> NeuronWebT t m () renderTags tags = do diff --git a/neuron/src/lib/Neuron/Zettelkasten/Graph/Build.hs b/neuron/src/lib/Neuron/Zettelkasten/Graph/Build.hs index e67a170de..71816fe26 100644 --- a/neuron/src/lib/Neuron/Zettelkasten/Graph/Build.hs +++ b/neuron/src/lib/Neuron/Zettelkasten/Graph/Build.hs @@ -15,7 +15,7 @@ import Neuron.Reader.Type import Neuron.Zettelkasten.Connection import Neuron.Zettelkasten.Graph.Type import Neuron.Zettelkasten.ID -import Neuron.Zettelkasten.Query.Error (QueryError) +import Neuron.Zettelkasten.Query.Error (QueryResultError) import Neuron.Zettelkasten.Query.Eval (queryConnections) import Neuron.Zettelkasten.Zettel import Neuron.Zettelkasten.Zettel.Parser @@ -29,13 +29,16 @@ buildZettelkasten :: ) buildZettelkasten fs = let zs = parseZettels fs - (g, queryErrors) = mkZettelGraph $ filter (not . zettelUnlisted) $ sansContent <$> zs + (g, qErrs) = mkZettelGraph $ filter (not . zettelUnlisted) $ sansContent <$> zs errors = Map.unions [ fmap ZettelError_ParseError $ Map.fromList $ - lefts zs <&> (zettelID &&& zettelError), - fmap ZettelError_QueryErrors queryErrors + flip mapMaybe (lefts zs) $ \z -> + case zettelError z of + Just zerr -> Just (zettelID z, zerr) + _ -> Nothing, + fmap ZettelError_QueryResultErrors qErrs ] in (g, zs, errors) @@ -46,10 +49,10 @@ buildZettelkasten fs = mkZettelGraph :: [Zettel] -> ( ZettelGraph, - Map ZettelID (NonEmpty QueryError) + Map ZettelID (NonEmpty QueryResultError) ) mkZettelGraph zettels = - let res :: [(Zettel, ([(Connection, Zettel)], [QueryError]))] = + let res :: [(Zettel, ([(Connection, Zettel)], [QueryResultError]))] = flip fmap zettels $ \z -> (z, runQueryConnections zettels z) g :: ZettelGraph = G.mkGraphFrom zettels $ @@ -60,7 +63,7 @@ mkZettelGraph zettels = (zettelID z,) <$> merrs in (g, errors) -runQueryConnections :: [Zettel] -> Zettel -> ([(Connection, Zettel)], [QueryError]) +runQueryConnections :: [Zettel] -> Zettel -> ([(Connection, Zettel)], [QueryResultError]) runQueryConnections zettels z = flip runReader zettels $ do runWriterT $ queryConnections z diff --git a/neuron/src/lib/Neuron/Zettelkasten/ID.hs b/neuron/src/lib/Neuron/Zettelkasten/ID.hs index 44a3e5505..573ec8d15 100644 --- a/neuron/src/lib/Neuron/Zettelkasten/ID.hs +++ b/neuron/src/lib/Neuron/Zettelkasten/ID.hs @@ -98,6 +98,7 @@ allowedSpecialChars = '-', '.', -- Whitespace is essential for title IDs + -- This gets replaced with underscope in ID slug (see unsafeMkZettelID). ' ', -- Allow some puctuation letters that are common in note titles ',', diff --git a/neuron/src/lib/Neuron/Zettelkasten/Query.hs b/neuron/src/lib/Neuron/Zettelkasten/Query.hs index 4ba381776..5b7c837d2 100644 --- a/neuron/src/lib/Neuron/Zettelkasten/Query.hs +++ b/neuron/src/lib/Neuron/Zettelkasten/Query.hs @@ -33,10 +33,10 @@ import Relude runZettelQuery :: [Zettel] -> ZettelQuery r -> Either QueryResultError r runZettelQuery zs = \case - ZettelQuery_ZettelByID zid _ -> + ZettelQuery_ZettelByID zid conn -> case find ((== zid) . zettelID) zs of Nothing -> - Left $ QueryResultError_NoSuchZettel zid + Left $ QueryResultError_NoSuchZettel (Just conn) zid Just z -> Right z ZettelQuery_ZettelsByTag pats _mconn _mview -> @@ -67,7 +67,7 @@ runGraphQuery g = \case GraphQuery_BacklinksOf conn zid -> case getZettel zid g of Nothing -> - Left $ QueryResultError_NoSuchZettel zid + Left $ QueryResultError_NoSuchZettel conn zid Just z -> Right $ backlinks (maybe isJust (const (== conn)) conn) z g diff --git a/neuron/src/lib/Neuron/Zettelkasten/Query/Error.hs b/neuron/src/lib/Neuron/Zettelkasten/Query/Error.hs index 6bc018a1e..5a0b362e6 100644 --- a/neuron/src/lib/Neuron/Zettelkasten/Query/Error.hs +++ b/neuron/src/lib/Neuron/Zettelkasten/Query/Error.hs @@ -6,48 +6,19 @@ module Neuron.Zettelkasten.Query.Error where -import Data.Aeson +import Data.Aeson (FromJSON, ToJSON) import Neuron.Orphans () -import Neuron.Zettelkasten.ID (InvalidID, ZettelID (..)) +import Neuron.Zettelkasten.Connection (Connection) +import Neuron.Zettelkasten.ID (ZettelID (..)) import Relude -import Text.URI (URI) -import qualified Text.URI as URI - -type QueryError = Either QueryParseError QueryResultError - -data QueryParseError - = QueryParseError_InvalidID URI InvalidID - | QueryParseError_UnsupportedHost URI - deriving (Eq, Show, Generic, ToJSON, FromJSON) -- | Error in evaluating a query -- -- This error is only thrown when *using* (eg: in HTML) the query results. data QueryResultError - = QueryResultError_NoSuchZettel ZettelID + = QueryResultError_NoSuchZettel (Maybe Connection) ZettelID deriving (Eq, Show, Generic, ToJSON, FromJSON) -queryParseErrorUri :: QueryParseError -> URI -queryParseErrorUri = \case - QueryParseError_InvalidID uri _ -> uri - QueryParseError_UnsupportedHost uri -> uri - -showQueryError :: QueryError -> Text -showQueryError = \case - Left qe -> - showQueryParseError qe - Right re -> - showQueryResultError re - -showQueryParseError :: QueryParseError -> Text -showQueryParseError qe = - let uri = URI.render (queryParseErrorUri qe) - in uri <> ": " <> case qe of - QueryParseError_UnsupportedHost _uri -> - "unsupported host" - QueryParseError_InvalidID _uri e'' -> - "invalidID: " <> show e'' - showQueryResultError :: QueryResultError -> Text -showQueryResultError (QueryResultError_NoSuchZettel zid) = - "links to non-existant zettel: " <> zettelIDRaw zid +showQueryResultError (QueryResultError_NoSuchZettel _conn zid) = + "no such zettel: " <> zettelIDRaw zid diff --git a/neuron/src/lib/Neuron/Zettelkasten/Query/Eval.hs b/neuron/src/lib/Neuron/Zettelkasten/Query/Eval.hs index d091eb009..b5894c1aa 100644 --- a/neuron/src/lib/Neuron/Zettelkasten/Query/Eval.hs +++ b/neuron/src/lib/Neuron/Zettelkasten/Query/Eval.hs @@ -32,15 +32,15 @@ import Relude -- -- We need the full list of zettels, for running the query against. runQueryURILink :: - ( MonadError QueryError m, + ( MonadError QueryResultError m, MonadReader [Zettel] m ) => URILink -> m (Maybe (DSum ZettelQuery Identity)) runQueryURILink ul = do - mq <- liftEither $ first Left $ queryFromURILink ul + let mq = queryFromURILink ul flip traverse mq $ \q -> - either (throwError . Right) pure =<< runExceptT (runSomeZettelQuery q) + either throwError pure =<< runExceptT (runSomeZettelQuery q) -- Query connections in the given zettel -- @@ -48,24 +48,18 @@ runQueryURILink ul = do -- query result errors. queryConnections :: ( -- Errors are written aside, accumulating valid connections. - MonadWriter [QueryError] m, + MonadWriter [QueryResultError] m, -- Running queries requires the zettels list. MonadReader [Zettel] m ) => Zettel -> m [(Connection, Zettel)] queryConnections Zettel {..} = do - -- Report any query parse errors - case zettelError of - Right queryParseErrors -> - tell $ Left <$> queryParseErrors - Left _ -> - pure () fmap concat $ forM zettelQueries $ \someQ -> runExceptT (runSomeZettelQuery someQ) >>= \case Left e -> do - tell [Right e] + tell [e] pure mempty Right res -> pure $ getConnections res diff --git a/neuron/src/lib/Neuron/Zettelkasten/Query/Parser.hs b/neuron/src/lib/Neuron/Zettelkasten/Query/Parser.hs index 19d45fe58..f493cafaf 100644 --- a/neuron/src/lib/Neuron/Zettelkasten/Query/Parser.hs +++ b/neuron/src/lib/Neuron/Zettelkasten/Query/Parser.hs @@ -25,7 +25,6 @@ import Data.TagTree (TagNode (..), TagPattern, constructTag, mkTagPattern) import Neuron.Reader.Type (ZettelFormat (..)) import Neuron.Zettelkasten.Connection import Neuron.Zettelkasten.ID -import Neuron.Zettelkasten.Query.Error import Neuron.Zettelkasten.Query.Theme import Neuron.Zettelkasten.Zettel (ZettelQuery (..)) import Reflex.Dom.Pandoc (URILink (..)) @@ -36,7 +35,7 @@ import Text.URI.QQ (queryKey) import Text.URI.Util (getQueryParam, hasQueryFlag) -- | Parse a query if any from a Markdown link -queryFromURILink :: MonadError QueryParseError m => URILink -> m (Maybe (Some ZettelQuery)) +queryFromURILink :: URILink -> Maybe (Some ZettelQuery) queryFromURILink l@URILink {..} = queryFromURI (defaultConnection l) _uriLink_uri where @@ -50,10 +49,10 @@ queryFromURILink l@URILink {..} = else OrdinaryConnection -- | Parse a query from the given URI. -queryFromURI :: MonadError QueryParseError m => Connection -> URI -> m (Maybe (Some ZettelQuery)) +queryFromURI :: Connection -> URI -> Maybe (Some ZettelQuery) queryFromURI defConn uri = do let conn = fromMaybe defConn (queryConn uri) - liftEither . runMaybeT $ do + do -- Non-relevant parts of the URI should be empty guard $ isNothing $ URI.uriFragment uri case URI.uriScheme uri of @@ -64,18 +63,17 @@ queryFromURI defConn uri = do guard $ URI.uriAuthority uri == Left False (False, path) <- URI.uriPath uri pure path - (URI.unRText -> path) :| [] <- hoistMaybe shortLinkPath + (URI.unRText -> path) :| [] <- shortLinkPath zid <- - hoistMaybe $ - -- Allow raw filename (ending with ".md"). HACK: hardcoding - -- format, but we shouldn't. - getZettelID ZettelFormat_Markdown (toString path) - -- Before checking for direct use of ID - <|> rightToMaybe (parseZettelID path) + -- Allow raw filename (ending with ".md"). HACK: hardcoding + -- format, but we shouldn't. + getZettelID ZettelFormat_Markdown (toString path) + -- Before checking for direct use of ID + <|> rightToMaybe (parseZettelID path) pure $ Some $ ZettelQuery_ZettelByID zid conn Just (URI.unRText -> proto) -> do guard $ proto == "z" - zPath <- hoistMaybe $ fmap snd (URI.uriPath uri) + zPath <- fmap snd (URI.uriPath uri) let -- Found "z:" without a trailing slash noSlash = URI.uriAuthority uri == Left False -- Found "z:/" instead of "z:" @@ -84,13 +82,17 @@ queryFromURI defConn uri = do -- Parse z:/ (URI.unRText -> path) :| [] | hasSlash -> do - zid <- parseQueryZettelID uri path - pure $ Some $ ZettelQuery_ZettelByID zid conn + case parseZettelID path of + Left _ -> empty + Right zid -> + pure $ Some $ ZettelQuery_ZettelByID zid conn -- Parse z:zettel/ (URI.unRText -> "zettel") :| [URI.unRText -> path] | noSlash -> do - zid <- parseQueryZettelID uri path - pure $ Some $ ZettelQuery_ZettelByID zid conn + case parseZettelID path of + Left _ -> empty + Right zid -> + pure $ Some $ ZettelQuery_ZettelByID zid conn -- Parse z:zettels?... (URI.unRText -> "zettels") :| [] | noSlash -> do @@ -105,10 +107,6 @@ queryFromURI defConn uri = do pure $ Some $ ZettelQuery_TagZettel (constructTag tagNodes) _ -> empty -parseQueryZettelID :: MonadError QueryParseError m => URI -> Text -> m ZettelID -parseQueryZettelID uri = - liftEither . first (QueryParseError_InvalidID uri) . parseZettelID - tagPatterns :: URI -> Text -> [TagPattern] tagPatterns uri k = mkTagPattern <$> getParamValues uri diff --git a/neuron/src/lib/Neuron/Zettelkasten/Zettel.hs b/neuron/src/lib/Neuron/Zettelkasten/Zettel.hs index af91f3206..b4028527e 100644 --- a/neuron/src/lib/Neuron/Zettelkasten/Zettel.hs +++ b/neuron/src/lib/Neuron/Zettelkasten/Zettel.hs @@ -18,20 +18,20 @@ module Neuron.Zettelkasten.Zettel where -import Data.Aeson -import Data.Aeson.GADT.TH +import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON)) +import Data.Aeson.GADT.TH (deriveJSONGADT) import Data.Dependent.Sum.Orphans () -import Data.GADT.Compare.TH -import Data.GADT.Show.TH +import Data.GADT.Compare.TH (DeriveGEQ (deriveGEq)) +import Data.GADT.Show.TH (DeriveGShow (deriveGShow)) import Data.Graph.Labelled (Vertex (..)) import Data.Some import Data.TagTree (Tag, TagPattern (..)) import Data.Time.DateMayTime (DateMayTime) -import Neuron.Reader.Type -import Neuron.Zettelkasten.Connection -import Neuron.Zettelkasten.ID -import Neuron.Zettelkasten.Query.Error -import Neuron.Zettelkasten.Query.Theme +import Neuron.Reader.Type (ZettelFormat, ZettelParseError) +import Neuron.Zettelkasten.Connection (Connection) +import Neuron.Zettelkasten.ID (ZettelID) +import Neuron.Zettelkasten.Query.Error (QueryResultError) +import Neuron.Zettelkasten.Query.Theme (ZettelsView) import Relude hiding (show) import Text.Pandoc.Definition (Pandoc (..)) import Text.Show (Show (show)) @@ -62,7 +62,7 @@ data ZettelT content = Zettel zettelUnlisted :: Bool, -- | List of all queries in the zettel zettelQueries :: [Some ZettelQuery], - zettelError :: ContentError content, + zettelError :: Maybe ZettelParseError, zettelContent :: content } deriving (Generic) @@ -70,21 +70,13 @@ data ZettelT content = Zettel newtype MetadataOnly = MetadataOnly () deriving (Generic, ToJSON, FromJSON) -type family ContentError c where --- The list of queries that failed to parse. - ContentError Pandoc = [QueryParseError] --- When a zettel fails to parse, we use its raw text along with its parse error. - ContentError Text = ZettelParseError --- When working with zettel sans content, we gather both kinds of errors (above) - ContentError MetadataOnly = Either (ContentError Text) (ContentError Pandoc) - -- | All possible errors in a zettel -- -- NOTE: Unlike `ContentError MetadataOnly` this also includes QueryResultError -- (which can be determined only after *evaluating* the queries). data ZettelError = ZettelError_ParseError ZettelParseError - | ZettelError_QueryErrors (NonEmpty QueryError) + | ZettelError_QueryResultErrors (NonEmpty QueryResultError) | ZettelError_AmbiguousFiles (NonEmpty FilePath) deriving (Eq, Show, Generic, ToJSON, FromJSON) @@ -98,13 +90,11 @@ sansContent :: ZettelC -> Zettel sansContent = \case Left z -> z - { zettelError = Left $ zettelError z, - zettelContent = MetadataOnly () + { zettelContent = MetadataOnly () } Right z -> z - { zettelError = Right $ zettelError z, - zettelContent = MetadataOnly () + { zettelContent = MetadataOnly () } instance Eq (ZettelT c) where diff --git a/neuron/src/lib/Neuron/Zettelkasten/Zettel/Parser.hs b/neuron/src/lib/Neuron/Zettelkasten/Zettel/Parser.hs index 7b6c7fd73..6b4f12555 100644 --- a/neuron/src/lib/Neuron/Zettelkasten/Zettel/Parser.hs +++ b/neuron/src/lib/Neuron/Zettelkasten/Zettel/Parser.hs @@ -14,7 +14,6 @@ import Data.Some import Data.TagTree (Tag) import Neuron.Reader.Type import Neuron.Zettelkasten.ID -import Neuron.Zettelkasten.Query.Error import Neuron.Zettelkasten.Query.Parser (queryFromURILink) import Neuron.Zettelkasten.Zettel import qualified Neuron.Zettelkasten.Zettel.Meta as Meta @@ -33,7 +32,7 @@ parseZettel :: parseZettel format zreader fn zid s = do case zreader fn s of Left parseErr -> - Left $ Zettel zid format fn "Unknown" False [] Nothing False [] parseErr s + Left $ Zettel zid format fn "Unknown" False [] Nothing False [] (Just parseErr) s Right (meta, doc) -> let -- Determine zettel title (title, titleInBody) = case Meta.title =<< meta of @@ -41,7 +40,7 @@ parseZettel format zreader fn zid s = do Nothing -> fromMaybe (zettelIDRaw zid, False) $ do ((,True) . plainify . snd <$> getH1 doc) -- Accumulate queries - (queries, errors) = runWriter $ extractQueries doc + queries = extractQueries doc -- Determine zettel tags metaTags = fromMaybe [] $ Meta.tags =<< meta queryTags = getInlineTag `mapMaybe` queries @@ -49,19 +48,13 @@ parseZettel format zreader fn zid s = do -- Determine other metadata date = Meta.date =<< meta unlisted = fromMaybe False $ Meta.unlisted =<< meta - in Right $ Zettel zid format fn title titleInBody tags date unlisted queries errors doc + in Right $ Zettel zid format fn title titleInBody tags date unlisted queries Nothing doc where -- Extract all (valid) queries from the Pandoc document - extractQueries :: MonadWriter [QueryParseError] m => Pandoc -> m [Some ZettelQuery] + extractQueries :: Pandoc -> [Some ZettelQuery] extractQueries doc = - fmap catMaybes $ - forM (queryURILinks doc) $ \ul -> - case queryFromURILink ul of - Left e -> do - tell [e] - pure Nothing - Right v -> - pure v + catMaybes $ + queryFromURILink <$> queryURILinks doc getInlineTag :: Some ZettelQuery -> Maybe Tag getInlineTag = \case Some (ZettelQuery_TagZettel tag) -> Just tag diff --git a/neuron/test/Neuron/Zettelkasten/Query/ParserSpec.hs b/neuron/test/Neuron/Zettelkasten/Query/ParserSpec.hs index 68c7c1ced..2f2dfc754 100644 --- a/neuron/test/Neuron/Zettelkasten/Query/ParserSpec.hs +++ b/neuron/test/Neuron/Zettelkasten/Query/ParserSpec.hs @@ -29,60 +29,60 @@ spec = do let shortLink s = mkURILink s s it "parses custom/hash ID" $ do queryFromURILink (shortLink "foo-bar") - `shouldBe` Right (Just $ Some $ ZettelQuery_ZettelByID (unsafeMkZettelID "foo-bar") Folgezettel) + `shouldBe` (Just $ Some $ ZettelQuery_ZettelByID (unsafeMkZettelID "foo-bar") Folgezettel) it "even with ?cf" $ do queryFromURILink (shortLink "foo-bar?cf") - `shouldBe` Right (Just $ Some $ ZettelQuery_ZettelByID (unsafeMkZettelID "foo-bar") OrdinaryConnection) + `shouldBe` (Just $ Some $ ZettelQuery_ZettelByID (unsafeMkZettelID "foo-bar") OrdinaryConnection) it "parses prefixed short link" $ do queryFromURILink (shortLink "z:/foo-bar") - `shouldBe` Right (Just $ Some $ ZettelQuery_ZettelByID (unsafeMkZettelID "foo-bar") Folgezettel) + `shouldBe` (Just $ Some $ ZettelQuery_ZettelByID (unsafeMkZettelID "foo-bar") Folgezettel) it "resolves ambiguity using absolute URI" $ do queryFromURILink (shortLink "z:/tags") - `shouldBe` Right (Just $ Some $ ZettelQuery_ZettelByID (unsafeMkZettelID "tags") Folgezettel) + `shouldBe` (Just $ Some $ ZettelQuery_ZettelByID (unsafeMkZettelID "tags") Folgezettel) it "z:zettels" $ do queryFromURILink (shortLink "z:zettels") - `shouldBe` Right (Just $ Some $ ZettelQuery_ZettelsByTag [] Folgezettel def) + `shouldBe` (Just $ Some $ ZettelQuery_ZettelsByTag [] Folgezettel def) it "z:zettels?tag=foo" $ do queryFromURILink (shortLink "z:zettels?tag=foo") - `shouldBe` Right (Just $ Some $ ZettelQuery_ZettelsByTag [mkTagPattern "foo"] Folgezettel def) + `shouldBe` (Just $ Some $ ZettelQuery_ZettelsByTag [mkTagPattern "foo"] Folgezettel def) it "z:zettels?cf" $ do queryFromURILink (shortLink "z:zettels?cf") - `shouldBe` Right (Just $ Some $ ZettelQuery_ZettelsByTag [] OrdinaryConnection def) + `shouldBe` (Just $ Some $ ZettelQuery_ZettelsByTag [] OrdinaryConnection def) it "z:tags" $ do queryFromURILink (shortLink "z:tags") - `shouldBe` Right (Just $ Some $ ZettelQuery_Tags []) + `shouldBe` (Just $ Some $ ZettelQuery_Tags []) it "z:tags?filter=foo" $ do queryFromURILink (shortLink "z:tags?filter=foo") - `shouldBe` Right (Just $ Some $ ZettelQuery_Tags [mkTagPattern "foo"]) + `shouldBe` (Just $ Some $ ZettelQuery_Tags [mkTagPattern "foo"]) it "z:tag/foo" $ do queryFromURILink (shortLink "z:tag/foo") - `shouldBe` Right (Just $ Some $ ZettelQuery_TagZettel (Tag "foo")) + `shouldBe` (Just $ Some $ ZettelQuery_TagZettel (Tag "foo")) it "z:tag/foo/bar/baz" $ do queryFromURILink (shortLink "z:tag/foo/bar/baz") - `shouldBe` Right (Just $ Some $ ZettelQuery_TagZettel (Tag "foo/bar/baz")) + `shouldBe` (Just $ Some $ ZettelQuery_TagZettel (Tag "foo/bar/baz")) it "i18n" $ do let encodeUriPath = toText . E.encode . toString shortLinkUnicode s = mkURILink' (mkURI . encodeUriPath) s s queryFromURILink (shortLinkUnicode "计算机") - `shouldBe` Right (Just $ Some $ ZettelQuery_ZettelByID (unsafeMkZettelID "计算机") Folgezettel) + `shouldBe` (Just $ Some $ ZettelQuery_ZettelByID (unsafeMkZettelID "计算机") Folgezettel) let normalLink = mkURILink "some link text" describe "flexible links (regular markdown)" $ do it "Default connection type should be cf" $ do queryFromURILink (normalLink "foo-bar") - `shouldBe` Right (Just $ Some $ ZettelQuery_ZettelByID (unsafeMkZettelID "foo-bar") OrdinaryConnection) + `shouldBe` (Just $ Some $ ZettelQuery_ZettelByID (unsafeMkZettelID "foo-bar") OrdinaryConnection) it "Supports full filename instead of zettel ID" $ do queryFromURILink (normalLink "foo-bar.md") - `shouldBe` Right (Just $ Some $ ZettelQuery_ZettelByID (unsafeMkZettelID "foo-bar") OrdinaryConnection) + `shouldBe` (Just $ Some $ ZettelQuery_ZettelByID (unsafeMkZettelID "foo-bar") OrdinaryConnection) describe "non-connection links" $ do it "pass through normal links" $ do queryFromURILink (normalLink "https://www.srid.ca") - `shouldBe` Right Nothing + `shouldBe` Nothing queryFromURILink (normalLink "/static/resume.pdf") - `shouldBe` Right Nothing + `shouldBe` Nothing queryFromURILink (normalLink "/static/") - `shouldBe` Right Nothing + `shouldBe` Nothing queryFromURILink (normalLink "/static") - `shouldBe` Right Nothing + `shouldBe` Nothing mkURILink :: Text -> Text -> URILink mkURILink = diff --git a/neuron/test/Neuron/Zettelkasten/ZettelSpec.hs b/neuron/test/Neuron/Zettelkasten/ZettelSpec.hs index a95fb417b..40829cf2c 100644 --- a/neuron/test/Neuron/Zettelkasten/ZettelSpec.hs +++ b/neuron/test/Neuron/Zettelkasten/ZettelSpec.hs @@ -22,7 +22,7 @@ import Test.Hspec spec :: Spec spec = do let noQueries = mempty -- TODO: test queries - noError = Right mempty + noError = Nothing noContent = MetadataOnly () describe "sortZettelsReverseChronological" $ do let mkDay = fromGregorian 2020 3