Skip to content
Open
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
35 changes: 35 additions & 0 deletions src/Commands/CommandLineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ private static bool TryParseInputOptions(CommandLineOptions commandLineOptions,
{
"help" => "help",
"run" => "run",
"export" => "export",
_ => $"{name1} {name2}".Trim()
};

Expand All @@ -191,6 +192,7 @@ private static bool TryParseInputOptions(CommandLineOptions commandLineOptions,
"web get" => new WebGetCommand(),
"help" => new HelpCommand(),
"run" => new RunCommand(),
"export" => new ExportCommand(),
_ => new FindFilesCommand()
};

Expand All @@ -208,6 +210,7 @@ private static bool TryParseInputOptions(CommandLineOptions commandLineOptions,
TryParseFindFilesCommandOptions(command as FindFilesCommand, args, ref i, arg) ||
TryParseWebCommandOptions(command as WebCommand, args, ref i, arg) ||
TryParseRunCommandOptions(command as RunCommand, args, ref i, arg) ||
TryParseExportCommandOptions(command as ExportCommand, args, ref i, arg) ||
TryParseSharedCommandOptions(command, args, ref i, arg);
if (parsedOption) return true;

Expand Down Expand Up @@ -301,6 +304,38 @@ private static bool TryParseHelpCommandOptions(CommandLineOptions commandLineOpt
return parsed;
}

private static bool TryParseExportCommandOptions(ExportCommand command, string[] args, ref int i, string arg)
{
bool parsed = true;

if (command == null)
{
parsed = false;
}
else if (arg == "--format")
{
var formatArgs = GetInputOptionArgs(i + 1, args, max: 1);
command.Format = formatArgs.FirstOrDefault() ?? throw new CommandLineException("Missing format value");
i += formatArgs.Count();
}
else if (arg == "--output")
{
var outputArgs = GetInputOptionArgs(i + 1, args, max: 1);
command.OutputPath = outputArgs.FirstOrDefault() ?? throw new CommandLineException("Missing output path");
i += outputArgs.Count();
}
else if (!arg.StartsWith("--"))
{
command.Files.Add(arg);
}
else
{
parsed = false;
}

return parsed;
}

private static bool TryParseRunCommandOptions(RunCommand command, string[] args, ref int i, string arg)
{
bool parsed = true;
Expand Down
72 changes: 72 additions & 0 deletions src/Commands/ExportCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.IO;
using mdx.Exporters;

namespace mdx.Commands;

public class ExportCommand : Command
{
private static readonly MarkdownExporters Exporters = new();

public string Format { get; set; }
public string OutputPath { get; set; }
public List<string> Files { get; set; } = new();

public override string GetCommandName() => "export";

public override string GetUsage() =>
"mdx export [options] <files...>\n" +
"Options:\n" +
" --format Output format (pdf, docx, or pptx)\n" +
" --output Output file path\n\n" +
"Example:\n" +
" mdx export --format pdf --output output.pdf input.md";

public override bool IsEmpty() => Files.Count == 0;

public override Command Validate()
{
if (string.IsNullOrEmpty(Format))
{
throw new CommandLineException("Export format must be specified with --format");
}

if (!Exporters.SupportedFormats.Contains(Format.ToLowerInvariant()))
{
throw new CommandLineException($"Unsupported export format: {Format}. Supported formats: {string.Join(", ", Exporters.SupportedFormats)}");
}

if (string.IsNullOrEmpty(OutputPath))
{
throw new CommandLineException("Output path must be specified with --output");
}

if (Files.Count == 0)
{
throw new CommandLineException("At least one input file must be specified");
}

return this;
}

public override void Execute()
{
var exporter = Exporters.GetExporter(Format);
var combinedContent = new List<string>();

foreach (var file in Files)
{
if (!File.Exists(file))
{
throw new FileNotFoundException($"Input file not found: {file}");
}

var content = File.ReadAllText(file);
combinedContent.Add(content);
}

var finalContent = string.Join("\n\n", combinedContent);
exporter.Export(finalContent, OutputPath);
}
}
41 changes: 41 additions & 0 deletions src/Exporters/DocxMarkdownExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.IO;
using System.Linq;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using Markdig;

namespace mdx.Exporters;

public class DocxMarkdownExporter : IMarkdownExporter
{
private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();

public string OutputFormat => "docx";

public void Export(string markdownContent, string outputPath)
{
using var document = WordprocessingDocument.Create(outputPath, WordprocessingDocumentType.Document);
var mainPart = document.AddMainDocumentPart();
mainPart.Document = new Document();
var body = mainPart.Document.AppendChild(new Body());

// Parse markdown to HTML first (easier to process)
var html = Markdown.ToHtml(markdownContent, Pipeline);

// Simple HTML parsing to generate basic DOCX paragraphs
var lines = html.Split('\n')
.Select(l => l.Trim())
.Where(l => !string.IsNullOrWhiteSpace(l));

foreach (var line in lines)
{
var para = new Paragraph(new Run(new Text(line)));
body.AppendChild(para);
}

mainPart.Document.Save();
}
}
19 changes: 19 additions & 0 deletions src/Exporters/IMarkdownExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace mdx.Exporters;

/// <summary>
/// Interface for exporting markdown content to other file formats
/// </summary>
public interface IMarkdownExporter
{
/// <summary>
/// Get the supported output format extension (without dot)
/// </summary>
string OutputFormat { get; }

/// <summary>
/// Export markdown content to the target format
/// </summary>
/// <param name="markdownContent">The markdown content to export</param>
/// <param name="outputPath">Path where to save the exported file</param>
void Export(string markdownContent, string outputPath);
}
51 changes: 51 additions & 0 deletions src/Exporters/MarkdownExporters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace mdx.Exporters;

/// <summary>
/// Manages available markdown exporters
/// </summary>
public class MarkdownExporters
{
private readonly Dictionary<string, IMarkdownExporter> _exporters = new();

public MarkdownExporters()
{
RegisterExporters();
}

/// <summary>
/// Register all available exporters
/// </summary>
private void RegisterExporters()
{
RegisterExporter(new PdfMarkdownExporter());
RegisterExporter(new DocxMarkdownExporter());
RegisterExporter(new PptxMarkdownExporter());
}

/// <summary>
/// Register a single exporter
/// </summary>
private void RegisterExporter(IMarkdownExporter exporter)
{
_exporters[exporter.OutputFormat.ToLowerInvariant()] = exporter;
}

/// <summary>
/// Get an exporter for the specified format
/// </summary>
public IMarkdownExporter GetExporter(string format)
{
return _exporters.TryGetValue(format.ToLowerInvariant(), out var exporter)
? exporter
: throw new ArgumentException($"No exporter found for format: {format}");
}

/// <summary>
/// Get all supported export formats
/// </summary>
public IEnumerable<string> SupportedFormats => _exporters.Keys;
}
84 changes: 84 additions & 0 deletions src/Exporters/PdfMarkdownExporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Markdig;
using PuppeteerSharp;

namespace mdx.Exporters;

public class PdfMarkdownExporter : IMarkdownExporter
{
private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();

public string OutputFormat => "pdf";

public void Export(string markdownContent, string outputPath)
{
// Convert markdown to HTML
var html = GenerateHtml(markdownContent);

// Generate PDF using Puppeteer
GeneratePdfAsync(html, outputPath).GetAwaiter().GetResult();
}

private static string GenerateHtml(string markdown)
{
var htmlContent = Markdown.ToHtml(markdown, Pipeline);
return $@"
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.6;
padding: 2em;
max-width: 50em;
margin: auto;
}}
pre {{
background-color: #f6f8fa;
padding: 1em;
border-radius: 4px;
overflow-x: auto;
}}
code {{
font-family: 'Consolas', 'Monaco', monospace;
}}
img {{
max-width: 100%;
height: auto;
}}
</style>
</head>
<body>
{htmlContent}
</body>
</html>";
}

private static async Task GeneratePdfAsync(string html, string outputPath)
{
await new BrowserFetcher().DownloadAsync();

await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true });
await using var page = await browser.NewPageAsync();

await page.SetContentAsync(html);
await page.PdfAsync(outputPath, new PdfOptions
{
Format = PaperFormat.A4,
PrintBackground = true,
MarginOptions = new MarginOptions
{
Top = "1cm",
Right = "1cm",
Bottom = "1cm",
Left = "1cm"
}
});
}
}
Loading