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. |