Skip to content
Draft
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
24 changes: 24 additions & 0 deletions LiquidVictor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiquidVictor.Output.Hardcod
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiquidVictor.Output.RevealJs.Test", "tst\LiquidVictor.Output.RevealJs.Test\LiquidVictor.Output.RevealJs.Test.csproj", "{889EF7D6-8205-48F7-9B5A-BDC1FB410B59}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jupyter", "Jupyter", "{73C10BE8-6303-FD38-E8F8-91ACFB49323C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiquidVictor.Output.Jupyter", "src\LiquidVictor.Output.Jupyter\LiquidVictor.Output.Jupyter.csproj", "{11C86541-A6E1-45C2-B615-D876569A2B8B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiquidVictor.Output.Jupyter.Generator", "src\LiquidVictor.Output.Jupyter.Generator\LiquidVictor.Output.Jupyter.Generator.csproj", "{2FBC0ED7-2563-46AF-BD14-D976904C41FC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiquidVictor.Output.Jupyter.Test", "tst\LiquidVictor.Output.Jupyter.Test\LiquidVictor.Output.Jupyter.Test.csproj", "{784184C6-F7C3-4E60-A10F-20568D9B0A87}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -190,6 +198,18 @@ Global
{889EF7D6-8205-48F7-9B5A-BDC1FB410B59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{889EF7D6-8205-48F7-9B5A-BDC1FB410B59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{889EF7D6-8205-48F7-9B5A-BDC1FB410B59}.Release|Any CPU.Build.0 = Release|Any CPU
{11C86541-A6E1-45C2-B615-D876569A2B8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11C86541-A6E1-45C2-B615-D876569A2B8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11C86541-A6E1-45C2-B615-D876569A2B8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11C86541-A6E1-45C2-B615-D876569A2B8B}.Release|Any CPU.Build.0 = Release|Any CPU
{2FBC0ED7-2563-46AF-BD14-D976904C41FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2FBC0ED7-2563-46AF-BD14-D976904C41FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2FBC0ED7-2563-46AF-BD14-D976904C41FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2FBC0ED7-2563-46AF-BD14-D976904C41FC}.Release|Any CPU.Build.0 = Release|Any CPU
{784184C6-F7C3-4E60-A10F-20568D9B0A87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{784184C6-F7C3-4E60-A10F-20568D9B0A87}.Debug|Any CPU.Build.0 = Debug|Any CPU
{784184C6-F7C3-4E60-A10F-20568D9B0A87}.Release|Any CPU.ActiveCfg = Release|Any CPU
{784184C6-F7C3-4E60-A10F-20568D9B0A87}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -227,6 +247,10 @@ Global
{1558F3EB-DDDB-A645-987A-3BA34C93C3A6} = {4B938667-F28A-4D2A-BE8E-BFDD90C914DB}
{C1E3CE8D-F372-4B35-35C5-9EC379528689} = {D085731D-AB53-42F1-BF58-E298196EED03}
{889EF7D6-8205-48F7-9B5A-BDC1FB410B59} = {4B938667-F28A-4D2A-BE8E-BFDD90C914DB}
{73C10BE8-6303-FD38-E8F8-91ACFB49323C} = {C12150DC-60ED-498B-891F-EBAC3238AB24}
{11C86541-A6E1-45C2-B615-D876569A2B8B} = {73C10BE8-6303-FD38-E8F8-91ACFB49323C}
{2FBC0ED7-2563-46AF-BD14-D976904C41FC} = {73C10BE8-6303-FD38-E8F8-91ACFB49323C}
{784184C6-F7C3-4E60-A10F-20568D9B0A87} = {4B938667-F28A-4D2A-BE8E-BFDD90C914DB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0E9064E7-C5DA-4719-A022-41A7DAC987C8}
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ and maintaining these presentations much easer.
* Links and their thumbnails
* Code samples

* Some key features of the system may be:
* Generate presentations in multiple formats and with different themes and aspect ratios
* Allow the regeneration of all presentations containing a slide or image when that asset changes
* Support the labeling of states so that a presentation can be regenerated as it was at a particular point in time
* Render LaTeX mathematical expressions in slide content using MathJax
* Some key features of the system may be:
* Generate presentations in multiple formats and with different themes and aspect ratios
* Export presentations to Jupyter Notebook (`.ipynb`) format with RISE slideshow metadata
* Allow the regeneration of all presentations containing a slide or image when that asset changes
* Support the labeling of states so that a presentation can be regenerated as it was at a particular point in time
* Render LaTeX mathematical expressions in slide content using MathJax

## Features

Expand Down
11 changes: 6 additions & 5 deletions src/LV/LV.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@
<ProjectReference Include="..\LiquidVictor.Data.Hardcoded\LiquidVictor.Data.Hardcoded.csproj" />
<ProjectReference Include="..\LiquidVictor.Data.Postgres\LiquidVictor.Data.Postgres.csproj" />
<ProjectReference Include="..\LiquidVictor.Data.YamlFile\LiquidVictor.Data.YamlFile.csproj" />
<ProjectReference Include="..\LiquidVictor.Output.Powerpoint\LiquidVictor.Output.Powerpoint.csproj" />
<ProjectReference Include="..\LiquidVictor.Output.RevealJs.Generator\LiquidVictor.Output.RevealJs.Generator.csproj" />
<ProjectReference Include="..\LiquidVictor.Strategy.TableOfContents\LiquidVictor.Strategy.TableOfContents.csproj" />
<ProjectReference Include="..\LiquidVictor\LiquidVictor.csproj" />
</ItemGroup>
<ProjectReference Include="..\LiquidVictor.Output.Powerpoint\LiquidVictor.Output.Powerpoint.csproj" />
<ProjectReference Include="..\LiquidVictor.Output.Jupyter.Generator\LiquidVictor.Output.Jupyter.Generator.csproj" />
<ProjectReference Include="..\LiquidVictor.Output.RevealJs.Generator\LiquidVictor.Output.RevealJs.Generator.csproj" />
<ProjectReference Include="..\LiquidVictor.Strategy.TableOfContents\LiquidVictor.Strategy.TableOfContents.csproj" />
<ProjectReference Include="..\LiquidVictor\LiquidVictor.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Update="Messages.Designer.cs">
Expand Down
33 changes: 22 additions & 11 deletions src/LV/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,35 @@ internal static class ServiceCollectionExtensions
{
public static IServiceCollection AddPresentationBuilder(this IServiceCollection services, Configuration config)
{
switch (config.OutputEngineType.ToUpperInvariant())
{
case "REVEAL":
case "REVEALJS":
services
switch (config.OutputEngineType.ToUpperInvariant())
{
case "REVEAL":
case "REVEALJS":
services
.AddTransient<IPresentationBuilder>(c =>
{
var builderOptions = new LiquidVictor.Output.RevealJs.Entities.BuilderOptions()
{
BuildTitleSlide = config.BuildTitleSlide,
MakeSoloImagesFullScreen = config.MakeSoloImagesFullScreen
};
return new LiquidVictor.Output.RevealJs.Generator.Engine(config.TemplatePath, builderOptions);
});
break;
default:
throw new NotSupportedException($"Invalid Presentation Builder '{config.OutputEngineType};");
}
return new LiquidVictor.Output.RevealJs.Generator.Engine(config.TemplatePath, builderOptions);
});
break;
case "JUPYTER":
services
.AddTransient<IPresentationBuilder>(c =>
{
var builderOptions = new LiquidVictor.Output.Jupyter.Entities.BuilderOptions()
{
BuildTitleSlide = config.BuildTitleSlide
};
return new LiquidVictor.Output.Jupyter.Generator.Engine(builderOptions);
});
break;
default:
throw new NotSupportedException($"Invalid Presentation Builder '{config.OutputEngineType};");
}

return services;
}
Expand Down
216 changes: 216 additions & 0 deletions src/LiquidVictor.Output.Jupyter.Generator/Engine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using LiquidVictor.Entities;
using LiquidVictor.Interfaces;
using LiquidVictor.Output.Jupyter.Entities;
using LiquidVictor.Output.Jupyter.Extensions;

namespace LiquidVictor.Output.Jupyter.Generator;

public class Engine(BuilderOptions builderOptions) : IPresentationBuilder
{
private const string DefaultNotebookFilename = "presentation.ipynb";
private const string DefaultThemeName = "black";
private readonly BuilderOptions _builderOptions = builderOptions ?? throw new ArgumentNullException(nameof(builderOptions));

public void CompilePresentation(SlideDeck slideDeck)
{
ArgumentNullException.ThrowIfNull(slideDeck);
_ = BuildNotebookJson(slideDeck);
}

public void CreatePresentation(string filepath, SlideDeck slideDeck)
{
ArgumentNullException.ThrowIfNullOrEmpty(filepath);
ArgumentNullException.ThrowIfNull(slideDeck);

var outputFilePath = ResolveOutputFilePath(filepath);
var outputFolder = Path.GetDirectoryName(outputFilePath) ?? throw new InvalidOperationException("Could not determine output directory");
Directory.CreateDirectory(outputFolder);

var notebook = BuildNotebookJson(slideDeck);
File.WriteAllText(outputFilePath, notebook);
}

private string BuildNotebookJson(SlideDeck slideDeck)
{
var cells = new List<NotebookCell>();
if (_builderOptions.BuildTitleSlide)
cells.Add(ToCell(slideDeck.CreateTitleSlide()));

foreach (var slide in slideDeck.Slides.OrderBy(s => s.Key).Select(s => s.Value))
{
cells.Add(ToCell(slide));
if (!string.IsNullOrWhiteSpace(slide.Notes))
cells.Add(ToCell($"### Presenter Notes{Environment.NewLine}{Environment.NewLine}{slide.Notes}", "notes"));
}

var notebook = new NotebookDocument
{
Cells = cells,
Metadata = new NotebookMetadata
{
LiveReveal = new LiveRevealMetadata
{
Theme = string.IsNullOrWhiteSpace(slideDeck.ThemeName) ? DefaultThemeName : slideDeck.ThemeName,
Transition = slideDeck.Transition.ToString()
}
}
};

return JsonSerializer.Serialize(notebook, SerializerOptions);
}

private static NotebookCell ToCell(Slide slide)
{
ArgumentNullException.ThrowIfNull(slide);

var markdown = new StringBuilder();
if (!string.IsNullOrWhiteSpace(slide.Title))
{
markdown.Append("# ");
markdown.AppendLine(slide.Title);
markdown.AppendLine();
}

if (slide.BackgroundContent is not null)
markdown.AppendLine(ToMarkdown(slide.BackgroundContent));

foreach (var contentItem in slide.ContentItems.OrderBy(c => c.Key).Select(c => c.Value))
markdown.AppendLine(ToMarkdown(contentItem));

return ToCell(markdown.ToString(), "slide");
}

private static NotebookCell ToCell(string markdown, string slideType)
{
return new NotebookCell
{
Metadata = new CellMetadata
{
Slideshow = new SlideshowMetadata
{
SlideType = slideType
}
},
Source = ToSourceLines(markdown)
};
}

private static List<string> ToSourceLines(string content)
{
var normalized = content.Replace("\r\n", "\n", StringComparison.Ordinal);
if (normalized.Length == 0)
return [];

var lines = normalized.Split('\n');
var source = new List<string>(lines.Length);
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i];
source.Add(i < lines.Length - 1 ? $"{line}\n" : line);
}

return source;
}

private static string ToMarkdown(ContentItem contentItem)
{
ArgumentNullException.ThrowIfNull(contentItem);

if (contentItem.ContentType.StartsWith("text", StringComparison.OrdinalIgnoreCase))
return Encoding.UTF8.GetString(contentItem.Content);

if (contentItem.ContentType.StartsWith("image", StringComparison.OrdinalIgnoreCase))
{
var altText = string.IsNullOrWhiteSpace(contentItem.Title) ? contentItem.FileName : contentItem.Title;
return $"![{altText}]({contentItem.AsImageSource()})";
}

throw new NotSupportedException("Only Text and Image content is currently supported");
}

private static string ResolveOutputFilePath(string path)
{
if (path.EndsWith(".ipynb", StringComparison.OrdinalIgnoreCase))
return path;

return Path.Combine(path, DefaultNotebookFilename);
}

private static readonly JsonSerializerOptions SerializerOptions = new()
{
WriteIndented = true
};

private sealed class NotebookDocument
{
[JsonPropertyName("cells")]
public List<NotebookCell> Cells { get; set; } = [];
[JsonPropertyName("metadata")]
public NotebookMetadata Metadata { get; set; } = new();
[JsonPropertyName("nbformat")]
public int Nbformat { get; set; } = 4;
[JsonPropertyName("nbformat_minor")]
public int NbformatMinor { get; set; } = 5;
}

private sealed class NotebookCell
{
[JsonPropertyName("cell_type")]
public string CellType { get; set; } = "markdown";
[JsonPropertyName("metadata")]
public CellMetadata Metadata { get; set; } = new();
[JsonPropertyName("source")]
public List<string> Source { get; set; } = [];
}

private sealed class CellMetadata
{
[JsonPropertyName("slideshow")]
public SlideshowMetadata Slideshow { get; set; } = new();
}

private sealed class SlideshowMetadata
{
[JsonPropertyName("slide_type")]
public string SlideType { get; set; } = "slide";
}

private sealed class NotebookMetadata
{
[JsonPropertyName("kernelspec")]
public KernelSpecMetadata KernelSpec { get; set; } = new();
[JsonPropertyName("language_info")]
public LanguageInfoMetadata LanguageInfo { get; set; } = new();
[JsonPropertyName("livereveal")]
public LiveRevealMetadata LiveReveal { get; set; } = new();
}

private sealed class KernelSpecMetadata
{
[JsonPropertyName("display_name")]
public string DisplayName { get; set; } = "Python 3";
[JsonPropertyName("language")]
public string Language { get; set; } = "python";
[JsonPropertyName("name")]
public string Name { get; set; } = "python3";
}

private sealed class LanguageInfoMetadata
{
[JsonPropertyName("name")]
public string Name { get; set; } = "python";
}

private sealed class LiveRevealMetadata
{
[JsonPropertyName("autolaunch")]
public bool AutoLaunch { get; set; }
[JsonPropertyName("theme")]
public string Theme { get; set; } = DefaultThemeName;
[JsonPropertyName("transition")]
public string Transition { get; set; } = "slide";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AnalysisLevel>latest-all</AnalysisLevel>
<AnalysisMode>all</AnalysisMode>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\LiquidVictor.Output.Jupyter\LiquidVictor.Output.Jupyter.csproj" />
<ProjectReference Include="..\LiquidVictor\LiquidVictor.csproj" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions src/LiquidVictor.Output.Jupyter/Entities/BuilderOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace LiquidVictor.Output.Jupyter.Entities;

public class BuilderOptions
{
public bool BuildTitleSlide { get; set; } = true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using LiquidVictor.Entities;
using LiquidVictor.Extensions;

namespace LiquidVictor.Output.Jupyter.Extensions;

public static class ContentItemExtensions
{
public static string AsImageSource(this ContentItem contentItem)
{
ArgumentNullException.ThrowIfNull(contentItem);

if (contentItem.Content.Length == 0)
return contentItem.FileName;

return $"data:{contentItem.ContentType};base64,{contentItem.Content.AsBase64String()}";
}
}
Loading