packages feed

emanote-1.0.0.0: src/Emanote/Pandoc/Renderer/Url.hs

module Emanote.Pandoc.Renderer.Url
  ( urlResolvingSplice,
    plainifyWikiLinkSplice,
    renderSomeInlineRefWith,
  )
where

import Commonmark.Extensions.WikiLink qualified as WL
import Data.Text qualified as T
import Emanote.Model (Model)
import Emanote.Model qualified as M
import Emanote.Model.Link.Rel qualified as Rel
import Emanote.Model.Link.Resolve qualified as Resolve
import Emanote.Model.Note qualified as MN
import Emanote.Model.Title qualified as Tit
import Emanote.Pandoc.Link qualified as Link
import Emanote.Pandoc.Renderer (PandocInlineRenderer)
import Emanote.Route qualified as R
import Emanote.Route.SiteRoute qualified as SR
import Heist.Extra.Splices.Pandoc qualified as HP
import Heist.Extra.Splices.Pandoc qualified as Splices
import Heist.Extra.Splices.Pandoc.Ctx (ctxSansCustomSplicing)
import Heist.Interpreted qualified as HI
import Optics.Core (review)
import Relude
import Text.Pandoc.Definition qualified as B
import Text.Pandoc.Walk qualified as W

-- | Resolve all URLs in inlines (<a> and <img>)
urlResolvingSplice :: PandocInlineRenderer Model R.LMLRoute
urlResolvingSplice model _nf (ctxSansCustomSplicing -> ctx) noteRoute inl = do
  (inlRef, attr@(id', cls, otherAttrs), is, (url, tit)) <- Link.parseInlineRef inl
  let parentR = R.withLmlRoute R.routeParent noteRoute
  (uRel, mAnchor) <- Rel.parseUnresolvedRelTarget parentR (otherAttrs <> one ("title", tit)) url
  let rRel = Resolve.resolveUnresolvedRelTarget model uRel
  renderSomeInlineRefWith id (is, (url, tit)) rRel model ctx inl $ \sr ->
    case inlRef of
      Link.InlineLink -> do
        -- TODO: If uRel is `Rel.URTWikiLink (WL.WikiLinkEmbed, _)`, *and* it appears
        -- in B.Para (so do this in block-level custom splice), then embed it.
        -- We don't do this here, as this inline splice can't embed block elements.
        let (newIs, (newUrl', isNotEmaLink)) = replaceLinkNodeWithRoute model sr (is, url)
            newOtherAttrs = otherAttrs <> [openInNewTabAttr | M.inLiveServer model && isNotEmaLink]
            newAttr = (id', cls, newOtherAttrs)
            newUrl = newUrl' <> WL.anchorSuffix mAnchor
        pure $ HP.rpInline ctx $ B.Link newAttr newIs (newUrl, tit)
      Link.InlineImage -> do
        let (newIs, (newUrl, _)) =
              replaceLinkNodeWithRoute model sr (toList $ nonEmptyInlines url is, url)
        pure $ HP.rpInline ctx $ B.Image attr newIs (newUrl, tit)

openInNewTabAttr :: (Text, Text)
openInNewTabAttr =
  ("target", "_blank")

renderSomeInlineRefWith ::
  (a -> SR.SiteRoute) ->
  -- | AST Node attributes of @InlineRef@
  ([B.Inline], (Text, Text)) ->
  Rel.ResolvedRelTarget a ->
  Model ->
  Splices.RenderCtx ->
  B.Inline ->
  (a -> Maybe (HI.Splice Identity)) ->
  Maybe (HI.Splice Identity)
renderSomeInlineRefWith getSr (is, (url, tit)) rRel model (ctxSansCustomSplicing -> ctx) origInl f = do
  case rRel of
    Rel.RRTMissing -> do
      pure $ do
        raw <-
          HP.rpInline
            ctx
            ( tooltip
                "Link is broken"
                [ B.Strikeout $ one $ B.Str $ Link.unParseLink origInl,
                  B.Str "❌"
                ]
            )
        details <-
          HP.rpInline ctx $
            -- FIXME: This aside is meaningless for non-wikilink links (regular
            -- Markdown links)
            B.Span ("", ["emanote:error:aside"], []) $
              one $
                tooltip "Find notes containing this broken link" $
                  one $
                    B.Link B.nullAttr (one $ B.Emph $ one $ B.Str "backlinks") (url, "")
        if M.inLiveServer model
          then pure $ raw <> details
          else pure raw
    Rel.RRTAmbiguous srs -> do
      pure $ do
        raw <- HP.rpInline ctx (tooltip "Link is ambiguous" [B.Strikeout $ one $ B.Str $ Link.unParseLink origInl, B.Str "❗"])
        candidates <-
          fmap mconcat . sequence $
            toList srs
              <&> \(getSr -> sr) -> do
                let (rp, _) = M.withoutRoutePrism model
                    srRoute = toText $ review rp sr
                    (_newIs, (newUrl, isNotEmaLink)) = replaceLinkNodeWithRoute model sr (is, srRoute)
                    linkAttr = [openInNewTabAttr | M.inLiveServer model && isNotEmaLink]
                    newIs = one $ B.Str $ show sr
                HP.rpInline ctx $
                  B.Span ("", ["emanote:error:aside"], []) $
                    one $
                      tooltip (show sr <> " -> " <> srRoute) $
                        one $
                          B.Link ("", mempty, linkAttr) newIs (newUrl, tit)
        if M.inLiveServer model
          then pure $ raw <> candidates
          else pure raw
    Rel.RRTFound sr -> do
      f sr
  where
    tooltip :: Text -> [B.Inline] -> B.Inline
    tooltip s = B.Span ("", [], one ("title", s))

plainifyWikiLinkSplice :: PandocInlineRenderer Model R.LMLRoute
plainifyWikiLinkSplice _model _nf (ctxSansCustomSplicing -> ctx) _ inl = do
  s <- WL.wikiLinkInlineRendered inl
  pure $ HP.rpInline ctx $ B.Str s

inlinesWithWikiLinksPlainified :: [B.Inline] -> [B.Inline]
inlinesWithWikiLinksPlainified = W.walk $ \case
  (WL.wikiLinkInlineRendered -> Just s) ->
    B.Str s
  x -> x

replaceLinkNodeWithRoute ::
  HasCallStack =>
  Model ->
  SR.SiteRoute ->
  ([B.Inline], Text) ->
  ([B.Inline], (Text, Bool))
replaceLinkNodeWithRoute model r (inner, url) =
  ( inlinesWithWikiLinksPlainified $ nonEmptyLinkInlines model url (Just r) inner,
    let linkUrl = SR.siteRouteUrl model r
     in (linkUrl, "?" `T.isInfixOf` linkUrl)
  )
  where
    nonEmptyLinkInlines :: Model -> Text -> Maybe SR.SiteRoute -> [B.Inline] -> [B.Inline]
    nonEmptyLinkInlines model' url' mr = \case
      [] ->
        toList $
          nonEmptyInlines url $
            fromMaybe [] $
              siteRouteDefaultInnerText model' url' =<< mr
      x -> x

-- | Ensure that inlines list is non-empty, using the provided singleton value if necessary.
nonEmptyInlines :: Text -> [B.Inline] -> NonEmpty B.Inline
nonEmptyInlines x =
  fromMaybe (one $ B.Str x) . nonEmpty

siteRouteDefaultInnerText :: Model -> Text -> SR.SiteRoute -> Maybe [B.Inline]
siteRouteDefaultInnerText model url = \case
  SR.SiteRoute_MissingR _ -> Nothing
  SR.SiteRoute_AmbiguousR _ _ -> Nothing
  SR.SiteRoute_VirtualRoute _ -> Nothing
  SR.SiteRoute_ResourceRoute resR ->
    case resR of
      SR.ResourceRoute_LML lmlR ->
        Tit.toInlines . MN._noteTitle <$> M.modelLookupNoteByRoute lmlR model
      SR.ResourceRoute_StaticFile _ _ ->
        -- Just append a file: prefix, to existing wiki-link.
        pure $ B.Str "File:" : [B.Str url]