neuron-1.0.0.0: src/lib/Neuron/Web/Query/View.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Neuron.Web.Query.View
( renderQueryResult,
renderZettelLink,
renderZettelLinkIDOnly,
style,
)
where
import Clay (Css, em, (?))
import qualified Clay as C
import Data.Dependent.Sum (DSum (..))
import qualified Data.Map.Strict as Map
import Data.Some (Some (..))
import Data.TagTree
( Tag (..),
TagNode (..),
TagPattern (..),
constructTag,
foldTagTree,
tagMatchAny,
tagTree,
)
import qualified Data.Text as T
import Data.Tree (Forest, Tree (Node))
import Neuron.Web.Route
( NeuronWebT,
Route (..),
neuronRouteLink,
)
import Neuron.Web.Widget (elTime, semanticIcon)
import Neuron.Zettelkasten.Connection (Connection (Folgezettel))
import Neuron.Zettelkasten.ID (ZettelID (zettelIDRaw))
import Neuron.Zettelkasten.Query.Theme (LinkView (..), ZettelsView (..))
import Neuron.Zettelkasten.Zettel
( Zettel,
ZettelQuery (..),
ZettelT (..),
sortZettelsReverseChronological,
)
import Reflex.Dom.Core hiding (count, tag)
import Reflex.Dom.Pandoc (PandocBuilder, elPandocInlines)
import Relude
import Text.Pandoc.Definition (Inline)
-- | Render the query results.
renderQueryResult ::
PandocBuilder t m => Maybe [Inline] -> DSum ZettelQuery Identity -> NeuronWebT t m ()
renderQueryResult minner = \case
ZettelQuery_ZettelByID _zid conn :=> Identity target -> do
renderZettelLink (elPandocInlines <$> minner) (Just conn) Nothing target
q@(ZettelQuery_ZettelsByTag pats conn view) :=> Identity res -> do
el "section" $ do
renderQuery $ Some q
case zettelsViewGroupByTag view of
False ->
el "ul" $
forM_ res $ \z -> do
el "li" $
renderZettelLink Nothing (Just conn) (Just $ zettelsViewLinkView view) z
True ->
forM_ (Map.toList $ groupZettelsByTagsMatching pats res) $ \(tag, zettelGrp) -> do
el "section" $ do
elClass "span" "ui basic pointing below grey label" $ do
semanticIcon "tag"
text $ unTag tag
el "ul" $
forM_ zettelGrp $ \z ->
el "li" $
renderZettelLink Nothing (Just conn) (Just $ zettelsViewLinkView view) z
q@(ZettelQuery_Tags _) :=> Identity res -> do
el "section" $ do
renderQuery $ Some q
renderTagTree $ foldTagTree $ tagTree res
ZettelQuery_TagZettel tag :=> Identity () ->
renderInlineTag tag mempty $ do
text "#"
text $ unTag tag
where
-- TODO: Instead of doing this here, group the results in runQuery itself.
groupZettelsByTagsMatching pats matches =
fmap sortZettelsReverseChronological $
Map.fromListWith (<>) $
flip concatMap matches $ \z ->
flip concatMap (zettelTags z) $ \t -> [(t, [z]) | tagMatchAny pats t]
renderQuery :: DomBuilder t m => Some ZettelQuery -> m ()
renderQuery someQ =
elAttr "div" ("class" =: "ui horizontal divider" <> "title" =: "Neuron ZettelQuery") $ do
case someQ of
Some (ZettelQuery_ZettelByID _ _) ->
blank
Some (ZettelQuery_ZettelsByTag [] _mconn _mview) ->
text "All zettels"
Some (ZettelQuery_ZettelsByTag (fmap unTagPattern -> pats) _mconn _mview) -> do
let qs = toText $ intercalate ", " pats
desc = toText $ "Zettels tagged '" <> qs <> "'"
elAttr "span" ("class" =: "ui basic pointing below black label" <> "title" =: desc) $ do
semanticIcon "tags"
text qs
Some (ZettelQuery_Tags []) ->
text "All tags"
Some (ZettelQuery_Tags (fmap unTagPattern -> pats)) -> do
let qs = toText $ intercalate ", " pats
text $ "Tags matching '" <> qs <> "'"
Some (ZettelQuery_TagZettel _tag) -> do
blank
-- | Render a link to an individual zettel.
renderZettelLink ::
DomBuilder t m =>
-- | Link inner text
Maybe (m ()) ->
-- | Connection type to display
Maybe Connection ->
-- | Link theme
Maybe LinkView ->
Zettel ->
NeuronWebT t m ()
renderZettelLink mInner conn (fromMaybe def -> linkView) Zettel {..} = do
let connClass = show <$> conn
rawClass = either (const $ Just "raw") (const Nothing) zettelError
mextra =
case linkView of
LinkView_Default ->
Nothing
LinkView_ShowDate ->
elTime <$> zettelDate
LinkView_ShowID ->
Just $ el "tt" $ text $ zettelIDRaw zettelID
classes :: [Text] = catMaybes $ [Just "zettel-link-container"] <> [connClass, rawClass]
elClass "span" (T.intercalate " " classes) $ do
forM_ mextra $ \extra ->
elNoSnippetSpan ("class" =: "extra monoFont") $ do
extra
-- The extra space is so that double clicking on this extra text
-- doesn't select the title next.
text " "
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
where
linkTooltip =
-- If there is custom inner text, put zettel title in tooltip.
-- Otherwise put tags if any.
if isJust mInner
then Just $ "Zettel: " <> zettelTitle
else
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
Just s ->
( "data-tooltip" =: s
<> "data-inverted" =: ""
<> "data-position" =: "right center"
)
-- | Like `renderZettelLink` but when we only have ID in hand.
renderZettelLinkIDOnly :: DomBuilder t m => ZettelID -> NeuronWebT t m ()
renderZettelLinkIDOnly zid =
elClass "span" "zettel-link-container" $ do
elClass "span" "zettel-link" $ do
neuronRouteLink (Some $ Route_Zettel zid) mempty $ text $ zettelIDRaw zid
renderTagTree :: forall t m. DomBuilder t m => Forest (NonEmpty TagNode, Natural) -> NeuronWebT t m ()
renderTagTree t =
divClass "tag-tree" $
renderForest mempty t
where
renderForest :: [TagNode] -> Forest (NonEmpty TagNode, Natural) -> NeuronWebT t m ()
renderForest ancestors forest =
el "ul" $ do
forM_ forest $ \tree ->
el "li" $ renderTree ancestors tree
renderTree :: [TagNode] -> Tree (NonEmpty TagNode, Natural) -> NeuronWebT t m ()
renderTree ancestors (Node (tagNode, count) children) = do
renderTag ancestors (tagNode, count)
renderForest (ancestors <> toList tagNode) $ toList children
renderTag :: [TagNode] -> (NonEmpty TagNode, Natural) -> NeuronWebT t m ()
renderTag ancestors (tagNode, count) = do
let tag = constructTag $ maybe tagNode (<> tagNode) $ nonEmpty ancestors
tit = show count <> " zettels tagged"
cls = bool "" "inactive" $ count == 0
divClass "node" $ do
renderInlineTag tag ("class" =: cls <> "title" =: tit) $
text $ renderTagNode tagNode
renderTagNode :: NonEmpty TagNode -> Text
renderTagNode = \case
n :| (nonEmpty -> mrest) ->
case mrest of
Nothing ->
unTagNode n
Just rest ->
unTagNode n <> "/" <> renderTagNode rest
renderInlineTag :: DomBuilder t m => Tag -> Map Text Text -> m () -> NeuronWebT t m ()
renderInlineTag tag attr body =
neuronRouteLink (Some $ Route_Search $ Just tag) attr body
style :: Css
style = do
zettelLinkCss
"div.tag-tree" ? do
"div.node" ? do
C.fontWeight C.bold
"a.inactive" ? do
C.color "#555"
zettelLinkCss :: Css
zettelLinkCss = do
"span.zettel-link-container span.zettel-link a" ? do
C.fontWeight C.bold
C.textDecoration C.none
"span.zettel-link-container span.extra" ? do
C.color C.auto
"span.zettel-link-container.raw" ? do
C.border C.solid (C.px 1) C.red
"[data-tooltip]:after" ? do
C.fontSize $ em 0.7