diff --git a/Nodsoft.MoltenObsidian.Tool/Commands/Manifest/GenerateManifestCommand.cs b/Nodsoft.MoltenObsidian.Tool/Commands/Manifest/GenerateManifestCommand.cs
index d32142b..35fc9d4 100644
--- a/Nodsoft.MoltenObsidian.Tool/Commands/Manifest/GenerateManifestCommand.cs
+++ b/Nodsoft.MoltenObsidian.Tool/Commands/Manifest/GenerateManifestCommand.cs
@@ -1,9 +1,11 @@
using System.ComponentModel;
+using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using Nodsoft.MoltenObsidian.Vaults.FileSystem;
using Nodsoft.MoltenObsidian.Manifest;
+using Nodsoft.MoltenObsidian.Vault;
using Spectre.Console;
using Spectre.Console.Cli;
@@ -36,7 +38,11 @@ public sealed class GenerateManifestSettings : CommandSettings
[CommandOption("--debug", IsHidden = true)]
public bool DebugMode { get; set; }
+
+ [CommandOption("--watch"), Description("Watches the vault for changes and regenerates the manifest.")]
+ public bool Watch { get; set; }
+
public override ValidationResult Validate()
{
if (VaultPathStr is "" || (VaultPath = new(VaultPathStr)) is { Exists: false })
@@ -49,18 +55,16 @@ public override ValidationResult Validate()
return ValidationResult.Error($"The output path '{OutputPath}' does not exist.");
}
- if (!Force)
+ if (!Force && !VaultPath.GetDirectories().Any(static d => d.Name == ".obsidian"))
{
- if (!VaultPath.GetDirectories().Any(static d => d.Name == ".obsidian"))
- {
- return ValidationResult.Error($"The vault path '{VaultPath}' does not appear to be a valid Obsidian vault.");
- }
+ return ValidationResult.Error($"The vault path '{VaultPath}' does not appear to be a valid Obsidian vault.");
}
return ValidationResult.Success();
}
}
+
///
/// Provides a command that generates a manifest for a Molten Obsidian vault.
///
@@ -72,15 +76,15 @@ public override async Task ExecuteAsync(CommandContext context, GenerateMan
if (settings.DebugMode)
{
// Inform the user that we're in debug mode.
- AnsiConsole.MarkupLine(/*lang=md*/"[bold blue]Debug mode is enabled.[/]");
+ AnsiConsole.MarkupLine( /*lang=md*/"[bold blue]Debug mode is enabled.[/]");
}
-
+
// This is where the magic happens.
// We'll be using the Spectre.Console library to provide a nice CLI experience.
// Statuses at each step, and a nice summary at the end.
FileSystemVault vault = null!;
-
+
// First, load the Obsidian vault. This will validate the vault and load all the files.
AnsiConsole.Console.Status().Start("Loading vault...", _ =>
{
@@ -90,8 +94,6 @@ public override async Task ExecuteAsync(CommandContext context, GenerateMan
AnsiConsole.Console.MarkupLine(/*lang=md*/$"[grey]Ignoring folders:[/] {string.Join("[grey], [/]", settings.IgnoredFolders ?? ["*None*"])}");
AnsiConsole.Console.MarkupLine(/*lang=md*/$"[grey]Ignoring files:[/] {string.Join("[grey], [/]", settings.IgnoreFiles ?? ["*None*"])}");
}
-
-
settings.IgnoredFolders ??= FileSystemVault.DefaultIgnoredFolders.ToArray();
settings.IgnoreFiles ??= FileSystemVault.DefaultIgnoredFiles.ToArray();
@@ -101,62 +103,129 @@ public override async Task ExecuteAsync(CommandContext context, GenerateMan
});
AnsiConsole.MarkupLine(/*lang=md*/$"Loaded vault with [green]{vault.Files.Count}[/] files.");
-
- // Next, generate the manifest.
- RemoteVaultManifest manifest = null!;
- await AnsiConsole.Console.Status().StartAsync("Generating manifest...", async _ =>
- {
- // Generate the manifest.
- manifest = await VaultManifestGenerator.GenerateManifestAsync(vault);
- });
- AnsiConsole.MarkupLine(/*lang=md*/$"Generated manifest with [green]{manifest.Files.Count}[/] files.");
-
- // Finally, write the manifest to disk, at the specified location (or the vault root if not specified).
- FileInfo manifestFile = new(Path.Combine((settings.OutputPath ?? settings.VaultPath).FullName, RemoteVaultManifest.ManifestFileName));
-
- // Check if the file already exists.
- if (manifestFile.Exists)
+ await GenerateManifestAsync(vault, settings.VaultPath, settings.OutputPath, settings.DebugMode, file =>
{
- if (settings.Force)
+ if (settings.Force || settings.Watch)
{
// Warn the user that the file will be overwritten.
- AnsiConsole.MarkupLine(/*lang=md*/"[yellow]A manifest file already exists at the specified location, but [green]--force[/] was specified. Overwriting.[/]");
+ string forceFlagStr = settings switch
+ {
+ { Force: true } => "--force",
+ { Watch: true } => "--watch",
+ _ => throw new UnreachableException()
+ };
+
+ AnsiConsole.MarkupLine(/*lang=md*/$"[yellow]A manifest file already exists at the specified location, but [green]{forceFlagStr}[/] was specified. Overwriting.[/]");
}
else
{
// If it does, ask the user if they want to overwrite it.
bool overwrite = AnsiConsole.Prompt(new ConfirmationPrompt(/*lang=md*/"[yellow]The manifest file already exists. Overwrite?[/]"));
-
+
if (!overwrite)
{
// If they don't, abort.
- AnsiConsole.MarkupLine("[red]Aborted.[/]");
- return 1;
+ AnsiConsole.MarkupLine(/*lang=md*/"[red]Aborted.[/]");
+ return false;
}
-
+
// If they do, delete the file.
- manifestFile.Delete();
+ file.Delete();
}
- }
- await AnsiConsole.Console.Status().StartAsync("Writing manifest...", async _ =>
+ return true;
+ });
+
+ if (settings.Watch)
{
- // Write the new manifest to disk.
- await using FileStream stream = manifestFile.Open(FileMode.OpenOrCreate, FileAccess.Write);
- stream.SetLength(0);
-
-
-#pragma warning disable CA1869
- await JsonSerializer.SerializeAsync(stream, manifest, new JsonSerializerOptions
+ await AnsiConsole.Console.Status().StartAsync("Watching vault for changes...", async ctx =>
{
- WriteIndented = settings.DebugMode, // If debug mode is enabled, write the manifest with indentation.
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Use camelCase for property names.
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull // Don't write null values.
+ // Print a status message.
+ ctx.Spinner(Spinner.Known.Dots);
+ ctx.SpinnerStyle(Style.Parse("purple bold"));
+
+ vault.VaultUpdate += async (_, args) =>
+ {
+ try
+ {
+ if (args.Entity.Path is RemoteVaultManifest.ManifestFileName)
+ {
+ AnsiConsole.MarkupLine(/*lang=md*/"[grey]Manifest update detected. Ignoring...[/]");
+ return;
+ }
+
+ // Print a status message.
+ AnsiConsole.MarkupLine(/*lang=md*/$"[grey]Vault update detected (Entity name: [/]{args.Entity.Path}[grey], Change type: [/]{args.Type})");
+ AnsiConsole.MarkupLine(/*lang=md*/"[blue]Regenerating manifest...[/]");
+
+ // Regenerate the manifest.
+ await GenerateManifestAsync(vault, settings.VaultPath, settings.OutputPath, settings.DebugMode, _ => true);
+ }
+ catch (Exception e)
+ {
+ // Print an error message.
+ AnsiConsole.MarkupLine(/*lang=md*/"[red]An error occurred while regenerating the manifest:[/]");
+ AnsiConsole.WriteException(e, ExceptionFormats.ShortenEverything);
+ }
+ };
+
+ // Watch the vault for changes.
+ await Task.Delay(-1);
});
- });
+ }
- AnsiConsole.MarkupLine(/*lang=md*/$"Wrote manifest to [green link]{manifestFile.FullName}[/].");
return 0;
}
-}
\ No newline at end of file
+
+ internal static async Task GenerateManifestAsync(
+ IVault vault,
+ DirectoryInfo vaultPath,
+ DirectoryInfo? outputPath,
+ bool debugMode,
+ Func promptOverwrite
+ ) {
+ // Assert the vault as FileSystem-Based
+ if (vault is not FileSystemVault)
+ {
+ throw new InvalidOperationException("The vault must be a FileSystemVault to generate a manifest.");
+ }
+
+ // Next, generate the manifest.
+ AnsiConsole.Console.MarkupLine( /*lang=md*/"Generating manifest...");
+ RemoteVaultManifest manifest = await VaultManifestGenerator.GenerateManifestAsync(vault);
+
+ AnsiConsole.MarkupLine(/*lang=md*/$"Generated manifest with [green]{manifest.Files.Count}[/] files.");
+
+ // Write the manifest to disk, at the specified location (or the vault root if not specified).
+ FileInfo manifestFile = new(Path.Combine((outputPath ?? vaultPath).FullName, RemoteVaultManifest.ManifestFileName));
+
+ if (manifestFile.Exists && !promptOverwrite(manifestFile))
+ {
+ return manifest;
+ }
+
+ // Write the manifest to disk.
+ await WriteManifestFileAsync(manifestFile, manifest, debugMode);
+ return manifest;
+ }
+
+ private static async Task WriteManifestFileAsync(FileInfo file, RemoteVaultManifest manifest, bool debugMode)
+ {
+ AnsiConsole.MarkupLine(/*lang=md*/$"Writing manifest...");
+
+ // Write the new manifest to disk.
+ await using FileStream fs = file.Open(FileMode.OpenOrCreate, FileAccess.Write);
+ fs.SetLength(0);
+
+#pragma warning disable CA1869
+ await JsonSerializer.SerializeAsync(fs, manifest, new JsonSerializerOptions
+ {
+ WriteIndented = debugMode, // If debug mode is enabled, write the manifest with indentation.
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Use camelCase for property names.
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull // Don't write null values.
+ });
+
+ AnsiConsole.MarkupLine(/*lang=md*/$"Wrote manifest to [green link]{file.FullName}[/].");
+ }
+}
diff --git a/Nodsoft.MoltenObsidian.Tool/Commands/SSG/GenerateStaticSiteCommand.cs b/Nodsoft.MoltenObsidian.Tool/Commands/SSG/GenerateStaticSiteCommand.cs
index 89a9eef..a8223aa 100644
--- a/Nodsoft.MoltenObsidian.Tool/Commands/SSG/GenerateStaticSiteCommand.cs
+++ b/Nodsoft.MoltenObsidian.Tool/Commands/SSG/GenerateStaticSiteCommand.cs
@@ -1,5 +1,7 @@
using System.ComponentModel;
using JetBrains.Annotations;
+using Nodsoft.MoltenObsidian.Manifest;
+using Nodsoft.MoltenObsidian.Tool.Commands.Manifest;
using Nodsoft.MoltenObsidian.Vault;
using Spectre.Console;
using Spectre.Console.Cli;
@@ -12,7 +14,6 @@ namespace Nodsoft.MoltenObsidian.Tool.Commands.SSG;
/// Specifies the command line arguments for the .
///
[PublicAPI]
-
public sealed class GenerateStaticSiteCommandSettings : CommandSettings
{
[CommandOption("--from-folder "), Description("Path to the local moltenobsidian vault")]
@@ -34,14 +35,20 @@ public sealed class GenerateStaticSiteCommandSettings : CommandSettings
public bool DebugMode { get; set; }
[CommandOption("--ignored-files "), Description("Ignore these files when creating the static site.")]
- public string[]? IgnoredFiles { get; private set; } = FileSystemVault.DefaultIgnoredFiles.ToArray();
+ public string[]? IgnoredFiles { get; private set; } = [..FileSystemVault.DefaultIgnoredFiles];
- [CommandOption("--ignored-folders "), Description("Ignore an entire directoroy when creating the static site.")]
- public string[]? IgnoredFolders { get; private set; } = FileSystemVault.DefaultIgnoredFolders.ToArray();
+ [CommandOption("--ignored-folders "), Description("Ignore an entire directory when creating the static site.")]
+ public string[]? IgnoredFolders { get; private set; } = [..FileSystemVault.DefaultIgnoredFolders];
+ [CommandOption("--generate-manifest"), Description("Generate a manifest file for the local vault if missing")]
+ public bool GenerateManifest { get; private set; }
+
+ [CommandOption("--watch"), Description("Watch the vault for changes and regenerate the static site")]
+ public bool Watch { get; private set; }
+
public override ValidationResult Validate()
{
- if (!string.IsNullOrEmpty(LocalVaultPathString) && !string.IsNullOrEmpty(RemoteManifestUrlString))
+ if (LocalVaultPathString is not "" && RemoteManifestUrlString is not "")
{
return ValidationResult.Error("--from-url and --from-folder options cannot be used together");
}
@@ -56,6 +63,16 @@ public override ValidationResult Validate()
return ValidationResult.Error($"The output path '{OutputPath}' does not exist.");
}
+ if (GenerateManifest && RemoteManifestUrlString is not (null or ""))
+ {
+ return ValidationResult.Error("Cannot generate a manifest for a remote vault");
+ }
+
+ if (Watch && LocalVaultPath is null)
+ {
+ return ValidationResult.Error("You can only use --watch with local vaults.");
+ }
+
OutputPath ??= new(Environment.CurrentDirectory);
if (!string.IsNullOrEmpty(RemoteManifestUrlString))
@@ -87,43 +104,92 @@ public sealed class GenerateStaticSite : AsyncCommand ExecuteAsync(CommandContext context, GenerateStaticSiteCommandSettings settings)
{
IVault vault = await StaticSiteGenerator.CreateReadVaultAsync(settings);
+
+ if (settings.DebugMode)
+ {
+ AnsiConsole.Console.MarkupLine(/*lang=md*/$"[grey]Ignoring folders:[/] {string.Join("[grey], [/]", settings.IgnoredFolders ?? ["*None*"])}");
+ AnsiConsole.Console.MarkupLine(/*lang=md*/$"[grey]Ignoring files:[/] {string.Join("[grey], [/]", settings.IgnoredFiles ?? ["*None*"])}");
- await AnsiConsole.Status().StartAsync("Generating static assets.", async ctx =>
- {
- ctx.Status("Generating static assets.");
- ctx.Spinner(Spinner.Known.Noise);
- ctx.SpinnerStyle(Style.Parse("purple bold"));
+ AnsiConsole.Console.MarkupLine(settings.OutputPath is null
+ ? /*lang=md*/$"[grey]Output path defaulted to current directory: [/]{Environment.CurrentDirectory}"
+ : /*lang=md*/$"[grey]Output path set: [/]{settings.OutputPath}"
+ );
+ }
- if (settings.DebugMode)
- {
- AnsiConsole.Console.MarkupLine(/*lang=md*/$"[grey]Ignoring folders:[/] {string.Join("[grey], [/]", settings.IgnoredFolders ?? ["*None*"])}");
- AnsiConsole.Console.MarkupLine(/*lang=md*/$"[grey]Ignoring files:[/] {string.Join("[grey], [/]", settings.IgnoredFiles ?? ["*None*"])}");
+ string[] ignoredFiles = settings.IgnoredFiles ?? [..FileSystemVault.DefaultIgnoredFiles];
+ string[] ignoredFolders = settings.IgnoredFolders ?? [..FileSystemVault.DefaultIgnoredFolders];
- AnsiConsole.Console.MarkupLine(settings.OutputPath is null
- ? /*lang=md*/$"[grey]Output path defaulted to current directory: [/]{Environment.CurrentDirectory}"
- : /*lang=md*/$"[grey]Output path set: [/]{settings.OutputPath}"
- );
- }
+ RemoteVaultManifest manifest = null!;
+
+ if (settings.GenerateManifest)
+ {
+ await GenerateManifestCommand.GenerateManifestAsync(vault, settings.LocalVaultPath!, settings.OutputPath, settings.DebugMode,_ => true);
+ }
+
+ await WriteStaticFilesAsync(vault, settings.OutputPath!, ignoredFiles, ignoredFolders);
- string[] ignoredFiles = settings.IgnoredFiles ?? [];
- string[] ignoredFolders = settings.IgnoredFolders ?? [];
-
- foreach (KeyValuePair pathFilePair in vault.Files)
+ if (settings.Watch)
+ {
+ await AnsiConsole.Console.Status().StartAsync("Watching vault for changes...", async ctx =>
{
- if (StaticSiteGenerator.IsIgnored(pathFilePair.Key, ignoredFolders, ignoredFiles))
+ ctx.Spinner(Spinner.Known.Dots);
+ ctx.SpinnerStyle(Style.Parse("purple bold"));
+
+ // Watch for changes
+ vault.VaultUpdate += async (_, args) =>
{
- continue;
- }
+ try
+ {
+ if (args.Entity.Path is RemoteVaultManifest.ManifestFileName)
+ {
+ AnsiConsole.MarkupLine(/*lang=md*/"[grey]Manifest update detected. Ignoring...[/]");
+ return;
+ }
+
+ // Print a status message.
+ AnsiConsole.MarkupLine(/*lang=md*/$"[grey]Vault update detected (Entity name: [/]{args.Entity.Path}[grey], Change type: [/]{args.Type})");
+ AnsiConsole.MarkupLine(/*lang=md*/"[blue]Regenerating SSG assets...[/]");
+
+ if (settings.GenerateManifest)
+ {
+ await GenerateManifestCommand.GenerateManifestAsync(vault, settings.LocalVaultPath!, settings.OutputPath, settings.DebugMode,_ => true);
+ }
+
+ await WriteStaticFilesAsync(vault, settings.OutputPath!, ignoredFiles, ignoredFolders);
+ }
+ catch (Exception e)
+ {
+ // Print an error message.
+ AnsiConsole.MarkupLine(/*lang=md*/"[red]An error occurred while regenerating the manifest:[/]");
+ AnsiConsole.WriteException(e, ExceptionFormats.ShortenEverything);
+ }
+ };
+
+ await Task.Delay(-1);
+ });
+ }
+
+ return 0;
+ }
- List fileData = await StaticSiteGenerator.CreateOutputFilesAsync(settings.OutputPath!.ToString(), pathFilePair);
- await Task.WhenAll(fileData.Select(WriteDataAsync));
+ internal static async Task WriteStaticFilesAsync(IVault vault, DirectoryInfo outputDirectory, string[] ignoredFiles, string[] ignoredFolders)
+ {
+ AnsiConsole.MarkupLine(/*lang=md*/"Generating static assets...");
+
+ foreach (KeyValuePair pathFilePair in vault.Files)
+ {
+ if (StaticSiteGenerator.IsIgnored(pathFilePair.Key, ignoredFolders, ignoredFiles))
+ {
+ continue;
}
- });
- AnsiConsole.Console.MarkupLine(/*lang=md*/$"Wrote static files to [green link]{settings.OutputPath}[/].");
- return 0;
+ List fileData = await StaticSiteGenerator.CreateOutputFilesAsync(outputDirectory.ToString(), pathFilePair);
+ await Task.WhenAll(fileData.Select(WriteDataAsync));
+ }
+
+ AnsiConsole.Console.MarkupLine(/*lang=md*/$"Wrote static files to [green link]{outputDirectory.FullName}[/].");
}
-
+
private static async Task WriteDataAsync(InfoDataPair pair)
{
if (!pair.FileInfo.Directory!.Exists)
diff --git a/Vaults/Nodsoft.MoltenObsidian.Vaults.FileSystem/FileSystemVault.cs b/Vaults/Nodsoft.MoltenObsidian.Vaults.FileSystem/FileSystemVault.cs
index 66cd088..ef1ef72 100644
--- a/Vaults/Nodsoft.MoltenObsidian.Vaults.FileSystem/FileSystemVault.cs
+++ b/Vaults/Nodsoft.MoltenObsidian.Vaults.FileSystem/FileSystemVault.cs
@@ -51,8 +51,18 @@ public sealed class FileSystemVault : IWritableVault
///
public event IVault.VaultUpdateEventHandler? VaultUpdate;
-
+ ///
+ /// Provides a dictionary of file change locks, by last datetime modified.
+ /// This is used to mitigate duplicate dispatches of a given file change event.
+ ///
+ private readonly ConcurrentDictionary _fileChangeLocks = [];
+
+ ///
+ /// Debounce period between file changes.
+ ///
+ private static readonly TimeSpan DebouncePeriod = TimeSpan.FromMilliseconds(100);
+
///
/// Gets the default list of folders to ignore when loading a vault.
///
@@ -152,7 +162,7 @@ private static bool IsDirectory(string path)
return (attr & FileAttributes.Directory) is FileAttributes.Directory;
}
- private string ToRelativePath(string fullPath) => fullPath[(_directoryInfo.FullName.Length + 1)..];
+ private string ToRelativePath(string fullPath) => fullPath[(_directoryInfo.FullName.Length + 1)..].Replace('\\', '/');
private void OnItemCreated(object sender, FileSystemEventArgs e) => OnItemCreatedAsync(sender, e).AsTask().GetAwaiter().GetResult();
private async ValueTask OnItemCreatedAsync(object sender, FileSystemEventArgs e)
@@ -228,7 +238,32 @@ private async ValueTask OnItemChangedAsync(object sender, FileSystemEventArgs e)
{
if (_files.TryGetValue(relativePath, out IVaultFile? file))
{
- await (VaultUpdate?.Invoke(this, new(UpdateType.Update, file)) ?? new());
+ // Ensure hashes differ
+ (SemaphoreSlim semaphore, _) = _fileChangeLocks.GetOrAdd(relativePath, _ => (new(1, 1), DateTime.UnixEpoch));
+
+ if (semaphore.CurrentCount is 0)
+ {
+ return;
+ }
+
+ await semaphore.WaitAsync();
+
+ try
+ {
+ // Update w/ reference hash at time of lock
+ (_, DateTime lastChange) = _fileChangeLocks[relativePath];
+ DateTime current = DateTime.UtcNow;
+
+ if (lastChange.Add(DebouncePeriod) < current)
+ {
+ await (VaultUpdate?.Invoke(this, new(UpdateType.Update, file)) ?? new());
+ _fileChangeLocks[relativePath] = new(semaphore, current);
+ }
+ }
+ finally
+ {
+ semaphore.Release();
+ }
}
}
}
diff --git a/nodsoft_moltenobsidian_web/src/styles/_content.scss b/nodsoft_moltenobsidian_web/src/styles/_content.scss
index 6f678ce..356faec 100644
--- a/nodsoft_moltenobsidian_web/src/styles/_content.scss
+++ b/nodsoft_moltenobsidian_web/src/styles/_content.scss
@@ -47,8 +47,12 @@ table, .table {
.markdown-alert {
@mixin callout-color($name, $color) {
background-color: rgba($color, 0.1);
+
+ &-#{$name} {
+ background-color: rgba($color, 0.1)
+ }
- &-title {
+ &-#{$name} .markdown-alert-title {
color: $color;
svg {
@@ -80,8 +84,10 @@ table, .table {
// Colors
@include callout-color("info", rgb(0, 122, 255));
+ @include callout-color("note", rgb(0, 122, 255));
+ @include callout-color("tip", rgb(83, 223, 221));
}
-@include code-block-syntax("csharp") {
+@include code-block-syntax("csharp", "cs") {
@import "syntax/csharp";
}
\ No newline at end of file
diff --git a/nodsoft_moltenobsidian_web/src/styles/_global.scss b/nodsoft_moltenobsidian_web/src/styles/_global.scss
index c565206..24c6470 100644
--- a/nodsoft_moltenobsidian_web/src/styles/_global.scss
+++ b/nodsoft_moltenobsidian_web/src/styles/_global.scss
@@ -94,23 +94,23 @@ h1 {
}
h2 {
- @include font-styles(3.5em);
+ @include font-styles(3em);
}
h3 {
- @include font-styles(3em);
+ @include font-styles(2.5em);
}
h4 {
- @include font-styles(2.5em);
+ @include font-styles(2em);
}
h5 {
- @include font-styles(2em);
+ @include font-styles(1.5em);
}
h6 {
- @include font-styles(1.5em);
+ @include font-styles(1.25em);
}
@media (max-device-width: map-get($breakpoints, "md")) {
diff --git a/nodsoft_moltenobsidian_web/src/styles/_mixins.scss b/nodsoft_moltenobsidian_web/src/styles/_mixins.scss
index 7bd3c43..8d055c3 100644
--- a/nodsoft_moltenobsidian_web/src/styles/_mixins.scss
+++ b/nodsoft_moltenobsidian_web/src/styles/_mixins.scss
@@ -1,8 +1,8 @@
-@mixin font-styles($size, $weight: 700, $line-height: 1, $margin-bottom: 1em) {
+@mixin font-styles($size, $weight: 700, $line-height: 1, $margin-bottom: 0.5em) {
font-size: $size;
font-weight: $weight;
line-height: $line-height;
- margin-bottom: $margin-bottom;
+ margin-block-end: $margin-bottom;
}
@mixin bg-acrylic($background) {
@@ -17,8 +17,10 @@
font-size: 90%;
}
-@mixin code-block-syntax($lang) {
-.lang-#{$lang} .#{$lang} pre {
- @content;
+@mixin code-block-syntax($langs...) {
+ @each $lang in $langs {
+ .lang-#{$lang} .#{$lang} pre {
+ @content;
+ }
}
}
\ No newline at end of file
diff --git a/nodsoft_moltenobsidian_web/vault/.obsidian/graph.json b/nodsoft_moltenobsidian_web/vault/.obsidian/graph.json
new file mode 100644
index 0000000..e21a18d
--- /dev/null
+++ b/nodsoft_moltenobsidian_web/vault/.obsidian/graph.json
@@ -0,0 +1,22 @@
+{
+ "collapse-filter": true,
+ "search": "",
+ "showTags": false,
+ "showAttachments": false,
+ "hideUnresolved": false,
+ "showOrphans": true,
+ "collapse-color-groups": true,
+ "colorGroups": [],
+ "collapse-display": true,
+ "showArrow": false,
+ "textFadeMultiplier": 0,
+ "nodeSizeMultiplier": 1,
+ "lineSizeMultiplier": 1,
+ "collapse-forces": true,
+ "centerStrength": 0.518713248970312,
+ "repelStrength": 10,
+ "linkStrength": 1,
+ "linkDistance": 250,
+ "scale": 1,
+ "close": false
+}
\ No newline at end of file
diff --git a/nodsoft_moltenobsidian_web/vault/Tool/Index.md b/nodsoft_moltenobsidian_web/vault/Tool/Index.md
index c593ec1..a54e490 100644
--- a/nodsoft_moltenobsidian_web/vault/Tool/Index.md
+++ b/nodsoft_moltenobsidian_web/vault/Tool/Index.md
@@ -39,6 +39,3 @@ All subsequent commands follow the conventional POSIX structure :
moltenobsidian [subcommand] [-a|--arguments [value]]
```
-
-# Features
-
diff --git a/nodsoft_moltenobsidian_web/vault/Tool/Static Site Generation.md b/nodsoft_moltenobsidian_web/vault/Tool/Static Site Generation.md
index 1cc2c18..f8fe6e2 100644
--- a/nodsoft_moltenobsidian_web/vault/Tool/Static Site Generation.md
+++ b/nodsoft_moltenobsidian_web/vault/Tool/Static Site Generation.md
@@ -1,10 +1,14 @@
-# Vault export
+# Static Site Generation
## Premise
-Sometimes you would like to use your vault in non-ASP.NET applications.
-This feature allows you to export a vault to static html files to be used where you like.
+Most developers require a solution to export their Obsidian vault to HTML, when integrating in apps based outside of ASP.NET Core, like JavaScript frameworks.
+This feature allows for a turnkey solution in exporting a local or remote Obsidian vault to a set of HTML and YAML files, along with all elements proper to the Obsidian syntax as well-defined as necessary.
-## Usage
+> [!TIP]
+> The content of this very website you are browsing now, [is rendered using our SSG feature](https://github.com/Nodsoft/MoltenObsidian/blob/main/.github/workflows/deploy-astro-website.yml), and integrated into [our Astro website](https://github.com/Nodsoft/MoltenObsidian/tree/main/nodsoft_moltenobsidian_web).
+> A perfect example of MoltenObsidian working with Astro, a JavaScript framework.
+
+## Usage
Exporting a local MoltenObsidian vault to a specified directory goes as follows :
```sh
moltenobsidian ssg generate --from-folder "/path/to/local/vault" -o "/destination/directory"
@@ -12,8 +16,8 @@ moltenobsidian ssg generate --from-folder "/path/to/local/vault" -o "/destinatio
The `ssg` command also supports exporting remote vaults from HTTP or FTP :
```sh
-moltenobsidian ssg generate --from-url "https://url/to/remote/vault/moltenobsidian.manifest.json" -o "/destination/directory"
-moltenobsidian ssg generate --from-url "ftp://url/to/remote/vault/moltenobsidian.manifest.json" -o "/destination/directory"
+moltenobsidian ssg generate --from-url "https://url.to/remote/vault/moltenobsidian.manifest.json" -o "/destination/directory"
+moltenobsidian ssg generate --from-url "ftp://url.to/remote/vault/moltenobsidian.manifest.json" -o "/destination/directory"
```
> [!NOTE]
@@ -27,4 +31,13 @@ Below are the protocols currently supported for vault exports :
| Protocol | URI segment |
| -------- | --------------------- |
| HTTP | `http://`, `https://` |
-| FTP | `ftp://`, `ftps://` |
\ No newline at end of file
+| FTP | `ftp://`, `ftps://` |
+
+### Developer features
+Some features of the manifest command are specifically oriented for development and automation purposes.
+Here is a detailed account of some of the extra features baked into this command.
+
+| Flag | Description |
+| --------- | ---------------------------------------------------------------------- |
+| `--watch` | Continuously watches for changes and updates the manifest accordingly. |
+| `--debug` | Prints out extra information, similar to a verbose flag. |
diff --git a/nodsoft_moltenobsidian_web/vault/Tool/Vault Manifests.md b/nodsoft_moltenobsidian_web/vault/Tool/Vault Manifests.md
index f7d34c1..08df4cf 100644
--- a/nodsoft_moltenobsidian_web/vault/Tool/Vault Manifests.md
+++ b/nodsoft_moltenobsidian_web/vault/Tool/Vault Manifests.md
@@ -18,15 +18,25 @@ Similarly, there may be cases where you need to output the manifest to a separat
Finally, if the default list of excluded folders/files is not sufficient, you can overwrite the list using the `--exclude-folder` and `--exclude-file` arguments. These can be invoked multiple times in the same command, like so:
```sh
-moltenobsidian manifest generate "/path/to/vault/root"--exclude-folder ".obsidian" --exclude-folder ".git" --exclude-folder ".github"
+moltenobsidian manifest generate "/path/to/vault/root" --exclude-folder ".obsidian" --exclude-folder ".git" --exclude-folder ".github"
```
```sh
-moltenobsidian manifest generate "/path/to/vault/root"--exclude-file "my/secret/document.md" --exclude-file "secrets.json"
+moltenobsidian manifest generate "/path/to/vault/root" --exclude-file "my/secret/document.md" --exclude-file "secrets.json"
```
For reference, these are the default exclusions:
-| **Entity Type** | **Exclusions** |
-| --- | --- |
-| **Folders** | `.obsidian` `.git` `.vs` `.vscode` `node_modules` |
-| **Files** | `.DS_STORE` |
+| **Entity Type** | **Exclusions** |
+| --------------- | ------------------------------------------------- |
+| **Folders** | `.obsidian` `.git` `.vs` `.vscode` `node_modules` |
+| **Files** | `.DS_STORE` |
+
+### Developer features
+Some features of the manifest command are specifically oriented for development and automation purposes.
+Here is a detailed account of some of the extra features baked into this command.
+
+| Flag | Description |
+| --------- | ------------------------------------------------------------------------ |
+| `--force` | Forces any existing manifest found at the output path to be overwritten. |
+| `--watch` | Continuously watches for changes and updates the manifest accordingly. |
+| `--debug` | Prints out extra information, similar to a verbose flag. |