Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include YAML frontmatter in the Stork index #398

Merged
merged 18 commits into from
Dec 24, 2022
Merged
Show file tree
Hide file tree
Changes from 13 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- UI
- Add source map for Stork [\#391](https://github.com/srid/emanote/pull/391)
- Workaround for Prism.js and Tailwind CSS both using `table` class [\#320](https://github.com/srid/emanote/pull/396)
- Add option to include YAML frontmatter in the Stork index [\#398](https://github.com/srid/emanote/pull/398)
- Features
- Timeline backlinks recognize flexible daily notes suffixed with arbitrary string [\#395](https://github.com/srid/emanote/issues/395)
- Misc
Expand Down
2 changes: 2 additions & 0 deletions default/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ template:
# Whether this node in the sidebar tree should remain collapsed by default
# (unless a route in it is active)
collapsed: true
stork:
frontmatter-handling: omit

pandoc:
# Rewrite the class specified in Pandoc's Div and Span nodes. You can specify the class using
Expand Down
20 changes: 18 additions & 2 deletions docs/guide/html-template/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,27 @@ Emanote provides client-side full-text search using [Stork](https://stork-search

You can trigger search input in one of the following ways:

- Press `Ctrl+K` (or `⌘K` on macOS).
- Press `Ctrl+K` (or `⌘K` on macOS).
- Click the lens icon on the sidebar (if using the 'book' [[html-template|template layout]]) or top-right corner (if using [[neuron-layout]]).

## Including frontmatter content in the search

By default, Stork doesn't include the text in the frontmatter as searchable.

This can be changed by adding the following to `index.yaml`:

```yaml
template:
stork:
frontmatter-handling: ignore
```

This allows you to search by the tags present in the frontmatter, but you will also get potentially undesired results when searching words like `order` or `date`.

The possible values are `ignore`, `omit` and `parse`. Default `omit`. See the Stork [docs](https://stork-search.net/docs/config-ref#frontmatter_handling).

## Known issues

- In live server mode, you may find that the browser will fetch remote assets (the wasm file) from files.stork-search.net. See details [here](https://github.com/jameslittle230/stork/issues/317#issuecomment-1258682222).

[^1]: The Stork index file can be accessed at [`-/stork.st`](-/stork.st).
[^1]: The Stork index file can be accessed at [`-/stork.st`](-/stork.st).
2 changes: 1 addition & 1 deletion emanote.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: 2.4
name: emanote
version: 1.0.1.5
version: 1.0.1.6
license: AGPL-3.0-only
copyright: 2022 Sridhar Ratnakumar
maintainer: [email protected]
Expand Down
18 changes: 16 additions & 2 deletions src/Emanote/Model/Stork.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@ module Emanote.Model.Stork
where

import Control.Monad.Logger (MonadLoggerIO)
import Data.Default (Default (def))
import Data.IxSet.Typed qualified as Ix
import Emanote.Model.Meta (lookupRouteMeta)
import Emanote.Model.Note qualified as N
import Emanote.Model.Stork.Index (File (File), Input (Input), readOrBuildStorkIndex)
import Emanote.Model.Stork.Index
( Config (Config),
File (File),
Handling,
Input (Input),
readOrBuildStorkIndex,
)
import Emanote.Model.Title qualified as Tit
import Emanote.Model.Type (Model)
import Emanote.Model.Type qualified as M
Expand All @@ -19,7 +27,8 @@ import System.FilePath ((</>))

renderStorkIndex :: (MonadIO m, MonadLoggerIO m) => Model -> m LByteString
renderStorkIndex model = do
readOrBuildStorkIndex (model ^. M.modelStorkIndex) (Input $ storkFiles model)
let config = Config $ Input (storkFiles model) (frontmatterHandling model)
readOrBuildStorkIndex (model ^. M.modelStorkIndex) config

storkFiles :: Model -> [File]
storkFiles model =
Expand All @@ -29,3 +38,8 @@ storkFiles model =
((baseDir </>) $ R.withLmlRoute R.encodeRoute $ note ^. N.noteRoute)
(SR.siteRouteUrl model $ SR.lmlSiteRoute $ note ^. N.noteRoute)
(Tit.toPlain $ note ^. N.noteTitle)

frontmatterHandling :: Model -> Handling
frontmatterHandling model =
let indexRoute = M.modelIndexRoute model
in lookupRouteMeta def ("template" :| ["stork", "frontmatter-handling"]) indexRoute model
96 changes: 73 additions & 23 deletions src/Emanote/Model/Stork/Index.hs
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,23 @@ module Emanote.Model.Stork.Index
readOrBuildStorkIndex,
File (File),
Input (Input),
Config (Config),
Handling,
)
where

import Control.Monad.Logger (MonadLoggerIO)
import Data.Aeson (FromJSON, genericParseJSON)
import Data.Aeson qualified as Aeson
import Data.Default (Default (..))
import Data.Text qualified as T
import Data.Time (NominalDiffTime, diffUTCTime, getCurrentTime)
import Emanote.Prelude (log, logD, logW)
import Numeric (showGFloat)
import Relude
import System.Process.ByteString (readProcessWithExitCode)
import System.Which (staticWhich)
import Toml (TomlCodec, encode, list, string, text, (.=))
import Toml (Key, TomlCodec, diwrap, encode, list, string, table, text, textBy, (.=))

-- | In-memory Stork index tracked in a @TVar@
newtype IndexVar = IndexVar (TVar (Maybe LByteString))
Expand All @@ -31,8 +36,8 @@ newIndex =
clearStorkIndex :: (MonadIO m) => IndexVar -> m ()
clearStorkIndex (IndexVar var) = atomically $ writeTVar var mempty

readOrBuildStorkIndex :: (MonadIO m, MonadLoggerIO m) => IndexVar -> Input -> m LByteString
readOrBuildStorkIndex (IndexVar indexVar) input = do
readOrBuildStorkIndex :: (MonadIO m, MonadLoggerIO m) => IndexVar -> Config -> m LByteString
readOrBuildStorkIndex (IndexVar indexVar) config = do
readTVarIO indexVar >>= \case
Just index -> do
logD "STORK: Returning cached search index"
Expand All @@ -41,7 +46,7 @@ readOrBuildStorkIndex (IndexVar indexVar) input = do
-- TODO: What if there are concurrent reads? We probably need a lock.
-- And we want to encapsulate this whole thing.
logW "STORK: Generating search index (this may be expensive)"
(diff, !index) <- timeIt $ runStork input
(diff, !index) <- timeIt $ runStork config
log $ toText $ "STORK: Done generating search index in " <> showGFloat (Just 2) diff "" <> " seconds"
atomically $ modifyTVar' indexVar $ \_ -> Just index
pure index
Expand All @@ -57,9 +62,9 @@ readOrBuildStorkIndex (IndexVar indexVar) input = do
storkBin :: FilePath
storkBin = $(staticWhich "stork")

runStork :: MonadIO m => Input -> m LByteString
runStork input = do
let storkToml = handleTomlandBug $ Toml.encode inputCodec input
runStork :: MonadIO m => Config -> m LByteString
runStork config = do
let storkToml = handleTomlandBug $ Toml.encode configCodec config
(_, !index, _) <-
liftIO $
readProcessWithExitCode
Expand All @@ -79,8 +84,14 @@ runStork input = do
-- title (but why would they?)
T.replace "\\\\U" "\\U"

newtype Input = Input
{ inputFiles :: [File]
newtype Config = Config
{ configInput :: Input
}
deriving stock (Eq, Show)

data Input = Input
{ inputFiles :: [File],
inputFrontmatterHandling :: Handling
}
deriving stock (Eq, Show)

Expand All @@ -91,18 +102,57 @@ data File = File
}
deriving stock (Eq, Show)

fileCodec :: TomlCodec File
fileCodec =
File
<$> Toml.string "path"
.= filePath
<*> Toml.text "url"
.= fileUrl
<*> Toml.text "title"
.= fileTitle
data Handling
= Handling_Ignore
| Handling_Omit
| Handling_Parse
deriving stock (Eq, Show, Generic)

inputCodec :: TomlCodec Input
inputCodec =
Input
<$> Toml.list fileCodec "input.files"
.= inputFiles
instance Default Handling where
def = Handling_Omit

instance FromJSON Handling where
parseJSON = genericParseJSON handlingJSONOptions
where
handlingJSONOptions :: Aeson.Options
handlingJSONOptions =
Aeson.defaultOptions
{ Aeson.constructorTagModifier = toString . T.toLower . T.replace "Handling_" "" . toText
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jfpedroza Wanna take a stab at this?

Otherwise, I'll do the rest of the changes (including this one) tomorrow and merge.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'll give it a try.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package works on fieldLabelModifier, not constructorTagModifier so I can't use the public functions. And even with the internal functions, they don't work on the form Handling_Ignore, they expect camel case.

The only thing useful is applyFirst that allows you to do

Aeson.constructorTagModifier = applyFirst toLower . drop 9

It's not worth to add the package for that function. I added it and changed replace with drop since it avoids the converting to text and back dance.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right.

I actually should have suggested https://hackage.haskell.org/package/deriving-aeson because that's exactly what we need here.

cf. https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/deriving_via.html


configCodec :: TomlCodec Config
configCodec =
Config
<$> Toml.table inputCodec "input"
.= configInput
where
inputCodec :: TomlCodec Input
inputCodec =
Input
<$> Toml.list fileCodec "files"
.= inputFiles
<*> Toml.diwrap (handlingCodec "frontmatter_handling")
.= inputFrontmatterHandling
fileCodec :: TomlCodec File
fileCodec =
File
<$> Toml.string "path"
.= filePath
<*> Toml.text "url"
.= fileUrl
<*> Toml.text "title"
.= fileTitle
handlingCodec :: Toml.Key -> TomlCodec Handling
handlingCodec = textBy showHandling parseHandling
where
showHandling :: Handling -> Text
showHandling handling = case handling of
Handling_Ignore -> "Ignore"
Handling_Omit -> "Omit"
Handling_Parse -> "Parse"
parseHandling :: Text -> Either Text Handling
parseHandling handling = case handling of
"Ignore" -> Right Handling_Ignore
"Omit" -> Right Handling_Omit
"Parse" -> Right Handling_Parse
other -> Left $ "Unsupported value for frontmatter handling: " <> other