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

Custom mount points #523

Merged
merged 11 commits into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
20 changes: 19 additions & 1 deletion docs/guide/layer.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Layer system

Emanote's layer system allows you to "merge" multiple notebooks and treat them as if they were a single notebook. The `-L` option in the command line accepts layers, and you can specify multiple of them with the leftmost taking the most precedence.
Emanote's layer system allows you to "merge" multiple notebook directories and treat them as if they were a single notebook directory. The `-L` option in the command line accepts layers, and you can specify multiple of them with the leftmost taking the most precedence.

For example,

```sh
emanote -L ./docs1:./docs2 run
```

Internally, Emante merges both `docs1` and `docs2` folders and treats them as a single directory. Thus, both `docs1` and `docs2` can contain the same file, and the one in `docs1` will take precedence.

## "Default" layer

Expand All @@ -9,3 +17,13 @@ Emanote *implicitly* includes what is known as the "default" layer. Its contents
## Merge semantics

The default merge semantic is to replace with the file on the right layer. For some file types, special merge semantic applies. For example, [[yaml-config|YAML files]] are merged by deep merge, not file-level replacement. This is what allows you to create `index.yaml` that overrides only a subset of the default configuration.

## Mount point

Layers can be mounted at a specific path. For example, if you want to mount `docs1` at `/D1` and `docs2` at `/D2`, you can do so with:

```sh
emanote -L /docs1@D1;/docs2@D2 run
```

When two layers are mounted at distinct mount points it becomes impossible for there to be overlaps. This is useful to host sub-sites under a single site, such as in [this case](https://github.com/flake-parts/community.flake.parts).
1 change: 1 addition & 0 deletions emanote/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- 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"
- Layers can be mounted in sub-directories, enabling composition of distinct notebooks ([\#523](https://github.com/srid/emanote/pull/523))
- **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))
Expand Down
2 changes: 1 addition & 1 deletion emanote/emanote.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: 2.4
name: emanote
version: 1.3.14.3
version: 1.3.15.0
license: AGPL-3.0-only
copyright: 2022 Sridhar Ratnakumar
maintainer: [email protected]
Expand Down
27 changes: 20 additions & 7 deletions emanote/src/Emanote/CLI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

module Emanote.CLI (
Cli (..),
Layer (..),
Cmd (..),
parseCli,
cliParser,
Expand All @@ -17,37 +18,49 @@ import Relude
import UnliftIO.Directory (getCurrentDirectory)

data Cli = Cli
{ layers :: NonEmpty FilePath
{ layers :: NonEmpty Layer
, allowBrokenLinks :: Bool
, cmd :: Cmd
}

data Layer = Layer
{ path :: FilePath
, mountPoint :: Maybe FilePath
}

data Cmd
= Cmd_Ema Ema.CLI.Cli
| Cmd_Export

cliParser :: FilePath -> Parser Cli
cliParser cwd = do
layers <- pathList (one cwd)
layers <- layerList $ one $ Layer cwd Nothing
allowBrokenLinks <- switch (long "allow-broken-links" <> help "Report but do not fail on broken links")
cmd <-
fmap Cmd_Ema Ema.CLI.cliParser
<|> subparser (command "export" (info (pure Cmd_Export) (progDesc "Export metadata JSON")))
pure Cli {..}
where
pathList defaultPath = do
option pathListReader
layerList defaultPath = do
option layerListReader
$ mconcat
[ long "layers"
, short 'L'
, metavar "LAYERS"
, value defaultPath
, help "List of (semicolon delimited) notebook folders to 'union mount', with the left-side folders being overlaid on top of the right-side ones. The default layer is implicitly included at the end of this list."
]
pathListReader :: ReadM (NonEmpty FilePath)
pathListReader =
layerListReader :: ReadM (NonEmpty Layer)
layerListReader = do
let partition s =
T.breakOn "@" s
& second (\x -> if T.null s then Nothing else Just $ T.drop 1 x)
maybeReader $ \paths ->
nonEmpty $ fmap toString $ T.split (== ';') . toText $ paths
nonEmpty
$ fmap (uncurry Layer . bimap toString (fmap toString) . partition)
$ T.split (== ';')
. toText
$ paths

parseCli' :: FilePath -> ParserInfo Cli
parseCli' cwd =
Expand Down
2 changes: 1 addition & 1 deletion emanote/src/Emanote/Model/Stork.hs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ renderStorkIndex model = do
storkFiles :: Model -> [File]
storkFiles model =
flip mapMaybe (Ix.toList (model ^. M.modelNotes)) $ \note -> do
baseDir <- Loc.locPath . fst <$> note ^. N.noteSource
baseDir <- fst . 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
Expand Down
2 changes: 1 addition & 1 deletion emanote/src/Emanote/Source/Dynamic.hs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ emanoteSiteInput cliAct EmanoteConfig {..} = do
defaultLayer <- Loc.defaultLayer <$> liftIO Paths_emanote.getDataDir
instanceId <- liftIO UUID.nextRandom
storkIndex <- Stork.newIndex
let layers = Loc.userLayers (CLI.layers _emanoteConfigCli) <> one defaultLayer
let layers = Loc.userLayers ((CLI.path &&& CLI.mountPoint) <$> CLI.layers _emanoteConfigCli) <> one defaultLayer
initialModel = Model.emptyModel layers cliAct _emanoteConfigPandocRenderers _emanoteCompileTailwind instanceId storkIndex
scriptingEngine <- getEngine
Dynamic
Expand Down
29 changes: 16 additions & 13 deletions emanote/src/Emanote/Source/Loc.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ module Emanote.Source.Loc (
-- * Using a `Loc`
locResolve,
locPath,
locMountPoint,

-- * Dealing with layers of locs
LocLayers,
userLayersToSearch,
) where

Expand All @@ -28,42 +28,45 @@ import System.FilePath ((</>))
The order here matters. Top = higher precedence.
-}
data Loc
= -- | The Int argument specifies the precedence (lower value = higher precedence)
LocUser Int FilePath
= -- | The Int argument specifies the precedence (lower value = higher precedence). The last argument is "mount point"
LocUser Int FilePath (Maybe FilePath)
| -- | The default location (ie., emanote default layer)
LocDefault FilePath
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (Aeson.ToJSON)

type LocLayers = Set Loc

{- | List of user layers, highest precedent being at first.

This is useful to delay searching for content in layers.
-}
userLayersToSearch :: LocLayers -> [FilePath]
userLayersToSearch :: Set Loc -> [FilePath]
userLayersToSearch =
mapMaybe
( \case
LocUser _ fp -> Just fp
LocUser _ fp _ -> Just fp
LocDefault _ -> Nothing
)
. Set.toAscList

defaultLayer :: FilePath -> Loc
defaultLayer = LocDefault

userLayers :: NonEmpty FilePath -> Set Loc
userLayers :: NonEmpty (FilePath, Maybe FilePath) -> Set Loc
userLayers paths =
fromList
$ zip [1 ..] (toList paths)
<&> uncurry LocUser
<&> (\(a, (b, c)) -> LocUser a b c)

-- | Return the effective path of a file.
locResolve :: (Loc, FilePath) -> FilePath
locResolve (loc, fp) = locPath loc </> fp
locResolve (loc, fp) = fst (locPath loc) </> fp

locPath :: Loc -> FilePath
locPath :: Loc -> (FilePath, Maybe FilePath)
locPath = \case
LocUser _ fp -> fp
LocDefault fp -> fp
LocUser _ fp m -> (fp, m)
LocDefault fp -> (fp, Nothing)

locMountPoint :: Loc -> Maybe FilePath
locMountPoint = \case
LocUser _ _ mountPoint -> mountPoint
LocDefault _ -> Nothing
6 changes: 3 additions & 3 deletions emanote/src/Emanote/Source/Patch.hs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Emanote.Prelude (
logD,
)
import Emanote.Route qualified as R
import Emanote.Source.Loc (Loc, LocLayers, locResolve, userLayersToSearch)
import Emanote.Source.Loc (Loc, locResolve, userLayersToSearch)
import Emanote.Source.Pattern (filePatterns, ignorePatterns)
import Heist.Extra.TemplateState qualified as T
import Optics.Operators ((%~))
Expand All @@ -36,7 +36,7 @@ import UnliftIO.Directory (doesDirectoryExist)
-- | Map a filesystem change to the corresponding model change.
patchModel ::
(MonadIO m, MonadLogger m, MonadLoggerIO m) =>
LocLayers ->
Set Loc ->
(N.Note -> N.Note) ->
Stork.IndexVar ->
-- | Lua scripting engine
Expand All @@ -59,7 +59,7 @@ patchModel layers noteF storkIndexTVar scriptingEngine fpType fp action = do
-- | Map a filesystem change to the corresponding model change.
patchModel' ::
(MonadIO m, MonadLogger m) =>
LocLayers ->
Set Loc ->
(N.Note -> N.Note) ->
Stork.IndexVar ->
-- | Lua scripting engine
Expand Down
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
tagtree.jailbreak = true;
tailwind.broken = false;
tailwind.jailbreak = true;
unionmount.check = !pkgs.stdenv.isDarwin; # garnix: Slow M1 builder
emanote = { name, pkgs, self, super, ... }: {
check = false;
extraBuildDepends = [ pkgs.stork ];
Expand Down Expand Up @@ -175,8 +176,7 @@
package = config.packages.default;
sites = {
"docs" = {
layers = [ ./docs ];
layersString = [ "./docs" ];
layers = [{ path = ./docs; pathString = "./docs"; }];
allowBrokenLinks = true; # A couple, by design, in markdown.md
prettyUrls = true;
};
Expand Down
51 changes: 38 additions & 13 deletions nix/flake-module.nix
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,42 @@ in
description = "Emanote sites";
type = types.attrsOf (types.submodule {
options = {
layers = mkOption {
type = types.listOf types.path;
description = ''List of directory paths to run Emanote on'';
};
# HACK: I can't seem to be able to convert `path` to a
# relative local path; so this is necessary.
#
# cf. https://discourse.nixos.org/t/converting-from-types-path-to-types-str/19405?u=srid
layersString = mkOption {
type = types.listOf types.str;
description = ''Like `layers` but local (not in Nix store)'';
layers = lib.mkOption {
description = "List of layers to use for the site";
type = types.listOf (types.submodule ({ config, ... }: {
options = {
path = mkOption {
type = types.path;
description = ''Directory path to notes'';
};
# HACK: I can't seem to be able to convert `path` to a
# relative local path; so this is necessary.
#
# cf. https://discourse.nixos.org/t/converting-from-types-path-to-types-str/19405?u=srid
pathString = mkOption {
type = types.str;
description = ''Like `path` but local (not in Nix store)'';
default = builtins.toString config.path;
};
mountPoint = mkOption {
type = types.nullOr types.str;
description = ''Mount point for the layer'';
default = null;
};
outputs.layer = mkOption {
type = types.str;
description = ''Layer spec'';
readOnly = true;
default = if config.mountPoint == null then "${config.path}" else "${config.path}@${config.mountPoint}";
};
outputs.layerString = mkOption {
type = types.str;
description = ''Layer spec'';
readOnly = true;
default = if config.mountPoint == null then config.pathString else "${config.pathString}@${config.mountPoint}";
};
};
}));
};
# TODO: Consolidate all these options below with those of home-manager-module.nix
port = mkOption {
Expand Down Expand Up @@ -96,7 +121,7 @@ in
runtimeInputs = [ config.emanote.package ];
text =
let
layers = lib.concatStringsSep ";" cfg.layersString;
layers = lib.concatStringsSep ";" (builtins.map (x: x.outputs.layerString) cfg.layers);
in
''
set -xe
Expand All @@ -123,7 +148,7 @@ in
mkdir -p $out
cp ${configFile} $out/index.yaml
'';
layers = lib.concatStringsSep ";" cfg.layers;
layers = lib.concatStringsSep ";" (builtins.map (x: x.outputs.layer) cfg.layers);
in
pkgs.runCommand "emanote-static-website-${name}"
{ meta.description = "Contents of the statically-generated Emanote website for ${name}"; }
Expand Down