diff --git a/docs/guide/folder-note.md b/docs/guide/folder-note.md new file mode 100644 index 000000000..cb636a457 --- /dev/null +++ b/docs/guide/folder-note.md @@ -0,0 +1,14 @@ +--- +slug: folder-note +--- + +# Folder notes + +For every folder `foo/` in your notebook, if a `foo.md` exists alongside it, it is consider a "folder note" that is associated with the folder. Likewise, an associated `foo.yaml` servers as the [[yaml-config]] for the entire folder and its contents recursively. + +The children of folder notes become its [[folgezettel|folgezettel]] children by default. + +{#index} +## `index.md` + +Instead of `foo.md`, you can also use `foo/index.md` (or `index.org` if using [[orgmode]]) or `foo/index.yaml`. Internally, Emanote will treat them as `foo.md` and `foo.yaml` respectively. \ No newline at end of file diff --git a/docs/guide/folgezettel.md b/docs/guide/folgezettel.md index 536a3f982..aa8f89352 100644 --- a/docs/guide/folgezettel.md +++ b/docs/guide/folgezettel.md @@ -23,7 +23,7 @@ Here is the [[target-note]]# {#folder} ## Folder notes -By default, Emanote includes any directories in your note's path as vertices in the folgezettel graph. This makes the folder note a folgezettel parent of the child note. The contents of a folder, likewise, become folgezetten children of the folder note. For eg., in `foo/bar/qux.md`, "foo" is a folgezettel parent of "bar", and "bar" is a folgezettel parent of "qux". +By default, Emanote includes any directories in your note's path as vertices in the folgezettel graph. This makes the [[folder-note|folder note]] a folgezettel parent of the child note. The contents of a folder, likewise, become folgezetten children of the [[folder-note|folder note]]. For eg., in `foo/bar/qux.md`, "foo" is a folgezettel parent of "bar", and "bar" is a folgezettel parent of "qux". >[!note] > The top-level root folder is not considered a folgezettel by default, unless you specify it (as shown below). This is to support the use-case of the user having a *flat list* of notes *without* subdirectories, but connected through [[folgezettel]]. @@ -38,4 +38,4 @@ emanote: ``` >[!tip] -> Put this configuration in `foo.md` if you want to disable folder folgezettel for just that folder `foo/`. Put it in `foo.yaml` if you want to disable it for `foo/` and all subfolders under it recursively. Put it in top-level `index.yaml` to disable it for all folders. See [[yaml-config]] for more details. \ No newline at end of file +> Put this configuration in `foo.md` if you want to disable folder folgezettel for just that [[folder-note|folder note]] `foo/`. Put it in `foo.yaml` if you want to disable it for `foo/` and all subfolders under it recursively. Put it in top-level `index.yaml` to disable it for all folders. See [[yaml-config]] for more details. \ No newline at end of file diff --git a/docs/guide/html-template/sidebar.md b/docs/guide/html-template/sidebar.md index 73c1f7e25..da02630a2 100644 --- a/docs/guide/html-template/sidebar.md +++ b/docs/guide/html-template/sidebar.md @@ -4,7 +4,7 @@ In [[html-template|the default template]], the sidebar is rendered on the left s - The sidebar tree is collapsed by default. But this can be disabled by setting `template.sidebar.collapsed` to `false` in [[yaml-config]] - The ordering of children in the tree is determined in the following order (this is also the order in which [[query]] results are rendered by default): - 1. if `template.sidebar.folders-first` is set to `true` (default `false`), directories are put before single notes, respecting the other order criteria. Otherwise, directories and notes will be interleaved. + 1. if `template.sidebar.folders-first` is set to `true` (default `false`), directories (see [[folder-note]]) are put before single notes, respecting the other order criteria. Otherwise, directories and notes will be interleaved. 2. If the `order` [[yaml-config|frontmatter]] metadata exists, use that as the primary sort key. 3. If the note has a H1 title, use that as the secondary sort key; otherwise, use the note filename as the secondary sort key. diff --git a/docs/guide/yaml-config.md b/docs/guide/yaml-config.md index 246e61833..5695c1cb5 100644 --- a/docs/guide/yaml-config.md +++ b/docs/guide/yaml-config.md @@ -10,7 +10,7 @@ tags: [emanote/yaml/demo] # YAML configuration -Configure your site metadata, rendering configuration and such using YAML configuration. Create a `foo.yaml` alongside `foo.md` or `foo/` folder, and those settings apply only to that route. The YAML structure is the same as your Markdown frontmatter, and vice-versa. Settings in the YAML frontmatter apply onto that Markdown route only; whereas settings in an individual .yaml file apply to that entire sub-route tree. Emanote does a deep-merge of the parent YAML configurations, so you can have children override only what's necessary. This is sometimes known as ["data cascade"](https://www.11ty.dev/docs/data-cascade/). The final merged YAML structure is passed to the HTML templates, of which you have full rendering control over. +Configure your site metadata, rendering configuration and such using YAML configuration. Create a `foo.yaml` alongside `foo.md` (see [[folder-note]]) or `foo/` folder, and those settings apply only to that route. The YAML structure is the same as your Markdown frontmatter, and vice-versa. Settings in the YAML frontmatter apply onto that Markdown route only; whereas settings in an individual .yaml file apply to that entire sub-route tree. Emanote does a deep-merge of the parent YAML configurations, so you can have children override only what's necessary. This is sometimes known as ["data cascade"](https://www.11ty.dev/docs/data-cascade/). The final merged YAML structure is passed to the HTML templates, of which you have full rendering control over. Notice how this page's sidebar colorscheme has [changed to green]{.greenery}? View [the source of this page](https://github.com/srid/emanote/blob/master/docs/guide/yaml-config.md) to see the magic involved. That CSS greenery you just saw too comes from YAML. diff --git a/emanote/CHANGELOG.md b/emanote/CHANGELOG.md index dfdf4f77a..20cad4a10 100644 --- a/emanote/CHANGELOG.md +++ b/emanote/CHANGELOG.md @@ -6,6 +6,10 @@ - Obsidian-style callouts ([\#466](https://github.com/srid/emanote/pull/466)) - `emanote run --no-ws` option to disable WebSocket monitoring. This is useful for using Emanote to serve the HTML site directly on the internet, without needing to statically generate it. - Add query syntax for listing folgezetten children & parents ([\#476](https://github.com/srid/emanote/pull/476)) + - Native support for combining multiple notebooks + - Resolve ambiguities based on closer common ancestor ([\#498](https://github.com/srid/emanote/pull/498)) + - Support for folder "index.md" notes ([\#512](https://github.com/srid/emanote/pull/512)) + - Instead of "foo/qux.md", you can now create "foo/qux/index.md" - **BACKWARDS INCOMPTABILE** changes - `feed.siteUrl` is now `page.siteUrl` - A new HTML template layout "default" (unifies and) replaces both "book" and "note" layout. ([\#483](https://github.com/srid/emanote/pull/483)) diff --git a/emanote/emanote.cabal b/emanote/emanote.cabal index 8fb8ce874..f692fb7e8 100644 --- a/emanote/emanote.cabal +++ b/emanote/emanote.cabal @@ -1,6 +1,6 @@ cabal-version: 2.4 name: emanote -version: 1.3.12.0 +version: 1.3.13.0 license: AGPL-3.0-only copyright: 2022 Sridhar Ratnakumar maintainer: srid@srid.ca diff --git a/emanote/src/Emanote/Model/Note.hs b/emanote/src/Emanote/Model/Note.hs index 97307bdc8..3c81c6648 100644 --- a/emanote/src/Emanote/Model/Note.hs +++ b/emanote/src/Emanote/Model/Note.hs @@ -49,7 +49,7 @@ data Feed = Feed data Note = Note { _noteRoute :: R.LMLRoute - , _noteLayerLoc :: Maybe Loc + , _noteSource :: Maybe (Loc, FilePath) -- ^ The layer from which this note came. Nothing if the note was auto-generated. , _noteDoc :: Pandoc , _noteMeta :: Aeson.Value @@ -281,12 +281,12 @@ mkEmptyNoteWith someR (Pandoc mempty -> doc) = where meta = Aeson.Null -mkNoteWith :: R.LMLRoute -> Maybe Loc -> Pandoc -> Aeson.Value -> [Text] -> Note -mkNoteWith r layerLoc doc' meta errs = +mkNoteWith :: R.LMLRoute -> Maybe (Loc, FilePath) -> Pandoc -> Aeson.Value -> [Text] -> Note +mkNoteWith r src doc' meta errs = let (doc'', tit) = queryNoteTitle r doc' meta feed = queryNoteFeed meta doc = if null errs then doc'' else pandocPrepend (errorDiv errs) doc'' - in Note r layerLoc doc meta tit errs feed + in Note r src doc meta tit errs feed where -- Prepend to block to the beginning of a Pandoc document (never before H1) pandocPrepend :: B.Block -> Pandoc -> Pandoc @@ -309,14 +309,14 @@ parseNote :: (Loc, FilePath) -> Text -> m Note -parseNote scriptingEngine pluginBaseDir r (layerLoc, fp) s = do +parseNote scriptingEngine pluginBaseDir r src@(_, fp) s = do ((doc, meta), errs) <- runWriterT $ do case r of R.LMLRoute_Md _ -> parseNoteMarkdown scriptingEngine pluginBaseDir fp s R.LMLRoute_Org _ -> do parseNoteOrg s - pure $ mkNoteWith r (Just layerLoc) doc meta errs + pure $ mkNoteWith r (Just src) doc meta errs parseNoteOrg :: (MonadWriter [Text] m) => Text -> m (Pandoc, Aeson.Value) parseNoteOrg s = diff --git a/emanote/src/Emanote/Model/Stork.hs b/emanote/src/Emanote/Model/Stork.hs index 848ed386e..801ad59c4 100644 --- a/emanote/src/Emanote/Model/Stork.hs +++ b/emanote/src/Emanote/Model/Stork.hs @@ -33,7 +33,7 @@ renderStorkIndex model = do storkFiles :: Model -> [File] storkFiles model = flip mapMaybe (Ix.toList (model ^. M.modelNotes)) $ \note -> do - baseDir <- Loc.locPath <$> note ^. N.noteLayerLoc + baseDir <- Loc.locPath . fst <$> note ^. N.noteSource let fp = ((baseDir ) $ R.withLmlRoute R.encodeRoute $ note ^. N.noteRoute) ft = case note ^. N.noteRoute of R.LMLRoute_Md _ -> FileType_Markdown diff --git a/emanote/src/Emanote/Route/ModelRoute.hs b/emanote/src/Emanote/Route/ModelRoute.hs index d890408b6..aa344395e 100644 --- a/emanote/src/Emanote/Route/ModelRoute.hs +++ b/emanote/src/Emanote/Route/ModelRoute.hs @@ -104,5 +104,5 @@ mkLMLRouteFromFilePath fp = mkLMLRouteFromKnownFilePath :: LML -> FilePath -> Maybe LMLRoute mkLMLRouteFromKnownFilePath lmlType fp = case lmlType of - Md -> fmap LMLRoute_Md (R.mkRouteFromFilePath fp) - Org -> fmap LMLRoute_Org (R.mkRouteFromFilePath fp) + Md -> fmap LMLRoute_Md (R.mkRouteFromFilePath' True fp) + Org -> fmap LMLRoute_Org (R.mkRouteFromFilePath' True fp) diff --git a/emanote/src/Emanote/Route/R.hs b/emanote/src/Emanote/Route/R.hs index 0b7b86d6c..84aa9fe1b 100644 --- a/emanote/src/Emanote/Route/R.hs +++ b/emanote/src/Emanote/Route/R.hs @@ -29,10 +29,19 @@ instance (HasExt ext) => Show (R ext) where -- | Convert foo/bar. to a @R@ mkRouteFromFilePath :: forall a (ext :: FileType a). (HasExt ext) => FilePath -> Maybe (R ext) -mkRouteFromFilePath fp = do +mkRouteFromFilePath = mkRouteFromFilePath' False + +{- | Like `mkRouteFromFilePath` but drops the last slug if it's "index" + +Behaves like `mkRouteFromFilePath` for top-level files. +-} +mkRouteFromFilePath' :: forall a (ext :: FileType a). (HasExt ext) => Bool -> FilePath -> Maybe (R ext) +mkRouteFromFilePath' dropIndex fp = do base <- withoutKnownExt @_ @ext fp - let slugs = fromString . toString . T.dropWhileEnd (== '/') . toText <$> splitPath base - viaNonEmpty R slugs + slugs <- nonEmpty $ fromString . toString . T.dropWhileEnd (== '/') . toText <$> splitPath base + if dropIndex && length slugs > 1 && last slugs == "index" + then viaNonEmpty R $ init slugs + else pure $ R slugs mkRouteFromSlugs :: NonEmpty Slug -> R ext mkRouteFromSlugs = diff --git a/emanote/src/Emanote/Source/Patch.hs b/emanote/src/Emanote/Source/Patch.hs index 0dcd849ee..17a6863e9 100644 --- a/emanote/src/Emanote/Source/Patch.hs +++ b/emanote/src/Emanote/Source/Patch.hs @@ -98,7 +98,7 @@ patchModel' layers noteF storkIndexTVar scriptingEngine fpType fp action = do log $ "Removing note: " <> toText fp pure $ M.modelDeleteNote r R.Yaml -> - case R.mkRouteFromFilePath fp of + case R.mkRouteFromFilePath' True fp of Nothing -> pure id Just r -> case action of diff --git a/emanote/src/Emanote/View/Template.hs b/emanote/src/Emanote/View/Template.hs index aa3809d39..f92334363 100644 --- a/emanote/src/Emanote/View/Template.hs +++ b/emanote/src/Emanote/View/Template.hs @@ -148,6 +148,8 @@ renderLmlHtml :: Model -> MN.Note -> LByteString renderLmlHtml model note = do let r = note ^. MN.noteRoute meta = patchMeta $ Meta.getEffectiveRouteMetaWith (note ^. MN.noteMeta) r model + sourcePath = fromMaybe (R.withLmlRoute R.encodeRoute r) $ do + fmap snd $ note ^. MN.noteSource -- Force a doctype into the generated HTML as a workaround for Heist -- discarding it. See: https://github.com/srid/emanote/issues/216 withDoctype = ("\n" <>) @@ -169,7 +171,8 @@ renderLmlHtml model note = do "ema:note:title" ## C.titleSplice ctx (note ^. MN.noteTitle) "ema:note:source-path" ## - HI.textSplice (toText . R.withLmlRoute R.encodeRoute $ r) + HI.textSplice + $ toText sourcePath "ema:note:url" ## HI.textSplice (SR.siteRouteUrl model . SR.lmlSiteRoute $ (R.LMLView_Html, r)) "emaNoteFeedUrl" ## diff --git a/emanote/test/Emanote/Route/RSpec.hs b/emanote/test/Emanote/Route/RSpec.hs index c7b643aa8..f561cf2d5 100644 --- a/emanote/test/Emanote/Route/RSpec.hs +++ b/emanote/test/Emanote/Route/RSpec.hs @@ -25,6 +25,15 @@ mkRouteFromFilePathSpec = describe "mkRouteFromFilePath" $ do mkRouteFromFilePath "foo/bar.md" === Just r2 it "three slugs" . hedgehog $ do mkRouteFromFilePath "foo/bar/qux.md" === Just r3 + describe "dropIndex" $ do + it "index route" . hedgehog $ do + mkRouteFromFilePath' True "index.md" === Just rIndex + it "single slug" . hedgehog $ do + mkRouteFromFilePath' True "foo.md" === Just r1 + it "two slugs" . hedgehog $ do + mkRouteFromFilePath' True "foo/index.md" === Just r1 + it "three slugs" . hedgehog $ do + mkRouteFromFilePath' True "foo/bar/index.md" === Just r2 routeInitsSpec :: Spec routeInitsSpec = describe "routeInits" $ do