Skip to content
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
49 changes: 37 additions & 12 deletions ChatGPTExport/Exporters/HtmlExporter.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System.Net;
using System.Text.RegularExpressions;
using ChatGPTExport.Assets;
using ChatGPTExport.Models;
using Markdig;

namespace ChatGPTExport.Exporters
{
internal class HtmlExporter(IHtmlFormatter formatter) : IExporter
internal partial class HtmlExporter(IHtmlFormatter formatter) : IExporter
{
private readonly string LineBreak = Environment.NewLine;

Expand Down Expand Up @@ -35,28 +36,51 @@ public IEnumerable<string> Export(IAssetLocator assetLocator, Conversation conve
}

var markdownPipeline = GetPipeline();
var bodyHtml = strings.Select(p => GetHtmlChunks(p.Author, p.Content, markdownPipeline));
var bodyHtml = strings.Select(p => GetHtmlFragment(p.Author, p.Content, markdownPipeline));

var titleString = WebUtility.HtmlEncode(conversation.title);
string html = formatter.FormatHtmlPage(titleString, bodyHtml);
string html = formatter.FormatHtmlPage(
new HtmlPage()
{
Body = bodyHtml,
Title = titleString,
});

return [html];
}

private string GetHtmlChunks(Author author, string content, MarkdownPipeline markdownPipeline)
[GeneratedRegex("```(.*)")]
private static partial Regex MarkdownCodeBlockRegex();
private (bool HasCode, List<string> Languages) GetLanguages(string markdown)
{
var html = Markdown.ToHtml(content, markdownPipeline);
var codeBlockRegex = MarkdownCodeBlockRegex().Matches(markdown);
var languages = codeBlockRegex.Where(p => p.Groups.Count > 1).
Select(p => p.Groups[1].Value).
Where(p => string.IsNullOrWhiteSpace(p) == false).
Select(p => p.Trim()).
Distinct(StringComparer.OrdinalIgnoreCase).
Select(v => v.ToLowerInvariant()).
ToList();
return (codeBlockRegex.Count > 0, languages);
}

if(author.role == "user")
{
return formatter.FormatUserInput(html);
}
else
private HtmlFragment GetHtmlFragment(Author author, string markdown, MarkdownPipeline markdownPipeline)
{
var html = Markdown.ToHtml(markdown, markdownPipeline);


var lanugages = GetLanguages(markdown);

var fragment = new HtmlFragment()
{
return html;
}
Html = author.role == "user" ? formatter.FormatUserInput(html) : html,
HasCode = lanugages.HasCode,
Languages = lanugages.Languages,
};
return fragment;
}


private MarkdownPipeline GetPipeline()
{
var pipelineBuilder = new MarkdownPipelineBuilder()
Expand Down Expand Up @@ -87,5 +111,6 @@ private MarkdownPipeline GetPipeline()
}

public string GetExtension() => ".html";

}
}
20 changes: 20 additions & 0 deletions ChatGPTExport/Exporters/HtmlPage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace ChatGPTExport.Exporters
{
public class HtmlPage
{
public string Title { get; set; }
public IEnumerable<HtmlFragment> Body { get; set; }

public string GetBodyString() => string.Join(Environment.NewLine, Body);
public bool HasCode => Body.Any(p => p.HasCode);
public IReadOnlyCollection<string> Languages => Body.SelectMany(p => p.Languages).Distinct().ToList();
}

public class HtmlFragment
{
public string Html { get; set; }
public bool HasCode { get; set; }
public IReadOnlyCollection<string> Languages { get; set; }
public override string ToString() => Html;
}
}
15 changes: 6 additions & 9 deletions ChatGPTExport/Exporters/HtmlTemplate/BootstrapHtmlFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,30 @@

namespace ChatGPTExport.Exporters.HtmlTemplate
{
internal class BootstrapHtmlFormatter : IHtmlFormatter
internal class BootstrapHtmlFormatter(IHeaderProvider headerProvider) : IHtmlFormatter
{
public void ApplyMarkdownPipelineBuilder(MarkdownPipelineBuilder markdownPipelineBuilder)
{
markdownPipelineBuilder.UseBootstrap();
}

public string FormatHtmlPage(string titleString, IEnumerable<string> bodyHtml)
public string FormatHtmlPage(HtmlPage page)
{
return $$"""
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>{{titleString}}</title>
<title>{{page.Title}}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<script>hljs.highlightAll();</script>

{{headerProvider.GetHeaders(page)}}
</head>
<body class="container">
<div class="my-4">
<h1>{{titleString}}</h1>
<h1>{{page.Title}}</h1>
</div>
{{string.Join("", bodyHtml)}}
{{page.GetBodyString()}}
</body>
</html>
""";
Expand Down
38 changes: 38 additions & 0 deletions ChatGPTExport/Exporters/HtmlTemplate/HighlightHeaderProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace ChatGPTExport.Exporters.HtmlTemplate
{
internal class HighlightHeaderProvider : IHeaderProvider
{
const string version = "11.11.1";
private static readonly string[] additionalLanguages = [
"dockerfile",
"powershell",
"http",
"latex",
"ini",
];

public string GetHeaders(HtmlPage htmlPage)
{
return htmlPage.HasCode ? GetCodeBlock(htmlPage.Languages) : "";
}

private static string GetCodeBlock(IEnumerable<string> languages)
{
var additionalScripts = languages.Intersect(additionalLanguages).Select(p =>
$""" <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/{version}/languages/{p}.min.js"></script>"""
);

var additionalStriptsBlock = string.Join(Environment.NewLine, additionalScripts);
if (additionalStriptsBlock.Length > 0) {
additionalStriptsBlock += Environment.NewLine;
}

return $"""
<!-- highlight.js (dark theme) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/{version}/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/{version}/highlight.min.js"></script>
{additionalStriptsBlock} <script>hljs.highlightAll();</script>
""";
}
}
}
15 changes: 6 additions & 9 deletions ChatGPTExport/Exporters/HtmlTemplate/TailwindHtmlFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,40 @@

namespace ChatGPTExport.Exporters.HtmlTemplate
{
internal class TailwindHtmlFormatter : IHtmlFormatter
internal class TailwindHtmlFormatter(IHeaderProvider headerProvider) : IHtmlFormatter
{
public void ApplyMarkdownPipelineBuilder(MarkdownPipelineBuilder markdownPipelineBuilder)
{
}

public string FormatHtmlPage(string titleString, IEnumerable<string> bodyHtml)
public string FormatHtmlPage(HtmlPage page)
{
return $$"""
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>{{titleString}}</title>
<title>{{page.Title}}</title>

<!-- Tailwind (with Typography plugin) -->
<script>
window.tailwind = { config: { darkMode: 'class' } };
</script>
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>

<!-- highlight.js (dark theme) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
{{headerProvider.GetHeaders(page)}}
</head>
<body class="bg-neutral-900 text-neutral-100 antialiased">
<div class="container mx-auto max-w-4xl px-4 py-6">
<h1 class="text-3xl font-semibold mb-6">{{titleString}}</h1>
<h1 class="text-3xl font-semibold mb-6">{{page.Title}}</h1>
<div class="prose prose-invert leading-relaxed max-w-[80ch] md:max-w-[90ch]
prose-p:my-2 prose-li:my-1
prose-ul:list-disc prose-ol:list-decimal
prose-pre:overflow-x-auto
prose-hr:my-4
marker:text-neutral-400">
{{string.Join("", bodyHtml)}}
{{page.GetBodyString()}}
</div>
</div>
</body>
Expand Down
7 changes: 7 additions & 0 deletions ChatGPTExport/Exporters/IHeaderProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace ChatGPTExport.Exporters
{
internal interface IHeaderProvider
{
string GetHeaders(HtmlPage htmlPage);
}
}
5 changes: 2 additions & 3 deletions ChatGPTExport/Exporters/IHtmlFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@

using Markdig;
using Markdig;

namespace ChatGPTExport.Exporters
{
internal interface IHtmlFormatter
{
void ApplyMarkdownPipelineBuilder(MarkdownPipelineBuilder markdownPipelineBuilder);
string FormatHtmlPage(string titleString, IEnumerable<string> bodyHtml);
string FormatHtmlPage(HtmlPage page);
string FormatUserInput(string html);
}
}
3 changes: 2 additions & 1 deletion ChatGPTExport/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
try
{
var directoryInfos = result.GetValue(sourceDirectoryOption);
foreach (var directoryInfo in directoryInfos)

Check warning on line 37 in ChatGPTExport/Program.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
{
if (directoryInfo.GetFiles(searchPattern, SearchOption.AllDirectories).Length == 0)
{
Expand Down Expand Up @@ -141,7 +141,8 @@
if (parseResult.GetRequiredValue(htmlOption))
{
var htmlFormat = parseResult.GetRequiredValue(htmlFormatOption);
var formatter = htmlFormat == HtmlFormat.Bootstrap ? new BootstrapHtmlFormatter() as IHtmlFormatter : new TailwindHtmlFormatter();
var headerProvider = new HighlightHeaderProvider();
var formatter = htmlFormat == HtmlFormat.Bootstrap ? new BootstrapHtmlFormatter(headerProvider) as IHtmlFormatter : new TailwindHtmlFormatter(headerProvider);
exporters.Add(new HtmlExporter(formatter));
}

Expand All @@ -167,7 +168,7 @@
var directoryConversationsMap = conversationFiles
.Select(file => new
{
AssetLocator = new AssetLocator(fileSystem, file.Directory, destination, existingAssetLocator) as IAssetLocator,

Check warning on line 171 in ChatGPTExport/Program.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'sourceDirectory' in 'AssetLocator.AssetLocator(IFileSystem fileSystem, IDirectoryInfo sourceDirectory, IDirectoryInfo destinationDirectory, ExistingAssetLocator existingAssetLocator)'.
Conversations = GetConversations(file)
})
.Where(x => x.Conversations != null)
Expand Down
Loading