{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} module Emanote.Model.Note where import Commonmark.Extensions.WikiLink (plainify) import Commonmark.Extensions.WikiLink qualified as WL import Control.Monad.Logger (MonadLogger) import Control.Monad.Writer (MonadWriter (tell), WriterT, runWriterT) import Data.Aeson qualified as Aeson import Data.Aeson.Optics qualified as AO import Data.Default (Default (def)) import Data.IxSet.Typed (Indexable (..), IxSet, ixFun, ixList) import Data.IxSet.Typed qualified as Ix import Data.Map.Strict qualified as Map import Data.Text qualified as T import Emanote.Model.Note.Filter (applyPandocFilters) import Emanote.Model.SData qualified as SData import Emanote.Model.Title qualified as Tit import Emanote.Pandoc.BuiltinFilters (preparePandoc) import Emanote.Pandoc.Markdown.Parser qualified as Markdown import Emanote.Pandoc.Markdown.Syntax.HashTag qualified as HT import Emanote.Route qualified as R import Emanote.Route.Ext (FileType (Folder)) import Emanote.Route.R (R) import Network.URI.Slug (Slug) import Optics.Core ((%), (.~)) import Optics.TH (makeLenses) import Relude import System.FilePath (()) import Text.Pandoc (runPure) import Text.Pandoc.Builder qualified as B import Text.Pandoc.Definition (Pandoc (..)) import Text.Pandoc.Readers.Org (readOrg) import Text.Pandoc.Scripting (ScriptingEngine) import Text.Pandoc.Walk qualified as W data Feed = Feed { _feedEnable :: Bool , _feedTitle :: Maybe Text , _feedLimit :: Maybe Word } deriving stock (Eq, Ord, Show, Generic) deriving anyclass (Aeson.ToJSON, Aeson.FromJSON) data Note = Note { _noteRoute :: R.LMLRoute , _noteDoc :: Pandoc , _noteMeta :: Aeson.Value , _noteTitle :: Tit.Title , _noteErrors :: [Text] , _noteFeed :: Maybe Feed } deriving stock (Eq, Ord, Show, Generic) deriving anyclass (Aeson.ToJSON) newtype RAncestor = RAncestor {unRAncestor :: R 'R.Folder} deriving stock (Eq, Ord, Show, Generic) deriving anyclass (Aeson.ToJSON) type NoteIxs = '[ -- Route to this note R.LMLRoute , -- Allowed ways to wiki-link to this note. WL.WikiLink , -- HTML route for this note R 'R.Html , -- XML route for this note R 'R.Xml , -- Ancestor folder routes RAncestor , -- Parent folder R 'R.Folder , -- Tag HT.Tag , -- Alias route for this note. Can be "foo" or "foo/bar". NonEmpty Slug ] type IxNote = IxSet NoteIxs Note instance Indexable NoteIxs Note where indices = ixList (ixFun $ one . _noteRoute) (ixFun $ toList . noteSelfRefs) (ixFun $ one . noteHtmlRoute) (ixFun $ maybeToList . noteXmlRoute) (ixFun noteAncestors) (ixFun $ maybeToList . noteParent) (ixFun noteTags) (ixFun $ maybeToList . noteSlug) -- | All possible wiki-links that refer to this note. noteSelfRefs :: Note -> NonEmpty WL.WikiLink noteSelfRefs = routeSelfRefs . _noteRoute where routeSelfRefs :: R.LMLRoute -> NonEmpty WL.WikiLink routeSelfRefs = fmap snd . R.withLmlRoute (WL.allowedWikiLinks . R.unRoute) noteAncestors :: Note -> [RAncestor] noteAncestors = maybe [] (toList . fmap RAncestor . R.routeInits) . noteParent noteParent :: Note -> Maybe (R 'R.Folder) noteParent = R.withLmlRoute R.routeParent . _noteRoute hasChildNotes :: R 'Folder -> IxNote -> Bool hasChildNotes r = not . Ix.null . Ix.getEQ r noteTags :: Note -> [HT.Tag] noteTags = fmap HT.Tag . maybeToMonoid . lookupMeta (one "tags") noteSlug :: Note -> Maybe (NonEmpty Slug) noteSlug note = do slugPath :: Text <- lookupMeta (one "slug") note fmap R.unRoute $ R.mkRouteFromFilePath @_ @'R.AnyExt $ toString slugPath lookupMeta :: (Aeson.FromJSON a) => NonEmpty Text -> Note -> Maybe a lookupMeta k = SData.lookupAeson Nothing k . _noteMeta noteHasFeed :: Note -> Bool noteHasFeed = maybe False _feedEnable . _noteFeed queryNoteFeed :: Aeson.Value -> Maybe Feed queryNoteFeed meta = do feed <- SData.lookupAeson Nothing (one "feed") meta let title = SData.lookupAeson Nothing (one "title") feed let enable = SData.lookupAeson False (one "enable") feed let feedLimit = SData.lookupAeson Nothing (one "limit") feed pure $ Feed enable title feedLimit queryNoteTitle :: R.LMLRoute -> Pandoc -> Aeson.Value -> (Pandoc, Tit.Title) queryNoteTitle r doc meta = let yamlNoteTitle = fromString <$> SData.lookupAeson Nothing (one "title") meta fileNameTitle = Tit.fromRoute r notePandocTitle = do case r of R.LMLRoute_Md _ -> getPandocTitle doc R.LMLRoute_Org _ -> getPandocMetaTitle doc in fromMaybe (doc, fileNameTitle) $ fmap (doc,) yamlNoteTitle <|> fmap (withoutH1 doc,) notePandocTitle where getPandocTitle :: Pandoc -> Maybe Tit.Title getPandocTitle = fmap Tit.fromInlines . getPandocH1 where getPandocH1 :: Pandoc -> Maybe [B.Inline] getPandocH1 (Pandoc _ (B.Header 1 _ inlines : _rest)) = Just inlines getPandocH1 _ = Nothing getPandocMetaTitle :: Pandoc -> Maybe Tit.Title getPandocMetaTitle (Pandoc docMeta _) = do B.MetaInlines inlines <- B.lookupMeta "title" docMeta pure $ Tit.fromInlines inlines withoutH1 :: B.Pandoc -> B.Pandoc withoutH1 (B.Pandoc m (B.Header 1 _ _ : rest)) = B.Pandoc m rest withoutH1 x = x -- | The xml route intended by user for this note. noteXmlRoute :: Note -> Maybe (R 'R.Xml) noteXmlRoute note | noteHasFeed note = Just (coerce $ noteHtmlRoute note) | otherwise = Nothing -- | The HTML route intended by user for this note. noteHtmlRoute :: Note -> R 'R.Html noteHtmlRoute note@Note {..} = -- Favour slug if one exists, otherwise use the full path. case noteSlug note of Nothing -> R.withLmlRoute coerce _noteRoute Just slugs -> R.mkRouteFromSlugs slugs lookupNotesByHtmlRoute :: R 'R.Html -> IxNote -> [Note] lookupNotesByHtmlRoute htmlRoute = Ix.toList . Ix.getEQ htmlRoute lookupNotesByXmlRoute :: R 'R.Xml -> IxNote -> [Note] lookupNotesByXmlRoute xmlRoute = Ix.toList . Ix.getEQ xmlRoute lookupNotesByRoute :: (HasCallStack) => R.LMLRoute -> IxNote -> Maybe Note lookupNotesByRoute r ix = do res <- nonEmpty $ Ix.toList $ Ix.getEQ r ix case res of note :| [] -> pure note _ -> error $ "ambiguous notes for route " <> show r ancestorPlaceholderNote :: R.R 'Folder -> Note ancestorPlaceholderNote r = let placeHolder = [ folderListingQuery , -- TODO: Ideally, we should use semantic tags, like