mirror of
https://github.com/Warky-Devs/go-mdtopdf-helper.git
synced 2025-05-18 15:17:29 +00:00
332 lines
7.6 KiB
Go
332 lines
7.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/SebastiaanKlippert/go-wkhtmltopdf"
|
|
"github.com/gomarkdown/markdown"
|
|
"github.com/gomarkdown/markdown/parser"
|
|
)
|
|
|
|
const (
|
|
Windows = "windows"
|
|
Linux = "linux"
|
|
MacOS = "darwin"
|
|
)
|
|
|
|
type Converter struct {
|
|
inputDir string
|
|
recursive bool
|
|
parallel bool
|
|
hookMode bool
|
|
}
|
|
|
|
func init() {
|
|
if err := ensureWkhtmltopdfInPath(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error setting up PATH: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
func ensureWkhtmltopdfInPath() error {
|
|
var wkhtmlPath string
|
|
pathSeparator := string(os.PathListSeparator)
|
|
|
|
switch runtime.GOOS {
|
|
case Windows:
|
|
wkhtmlPath = `C:\Program Files\wkhtmltopdf\bin`
|
|
case Linux:
|
|
// Common Linux installation paths
|
|
possiblePaths := []string{
|
|
"/usr/local/bin",
|
|
"/usr/bin",
|
|
"/opt/wkhtmltopdf/bin",
|
|
}
|
|
|
|
for _, path := range possiblePaths {
|
|
if _, err := os.Stat(filepath.Join(path, "wkhtmltopdf")); err == nil {
|
|
wkhtmlPath = path
|
|
break
|
|
}
|
|
}
|
|
case MacOS:
|
|
// Common MacOS installation paths
|
|
possiblePaths := []string{
|
|
"/usr/local/bin",
|
|
"/opt/homebrew/bin",
|
|
"/opt/local/bin",
|
|
}
|
|
|
|
for _, path := range possiblePaths {
|
|
if _, err := os.Stat(filepath.Join(path, "wkhtmltopdf")); err == nil {
|
|
wkhtmlPath = path
|
|
break
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
|
}
|
|
|
|
if wkhtmlPath == "" {
|
|
return fmt.Errorf("wkhtmltopdf not found in common installation paths")
|
|
}
|
|
|
|
// Check if the directory exists
|
|
if _, err := os.Stat(wkhtmlPath); os.IsNotExist(err) {
|
|
return fmt.Errorf("wkhtmltopdf directory not found at: %s", wkhtmlPath)
|
|
}
|
|
|
|
// Get current PATH
|
|
currentPath := os.Getenv("PATH")
|
|
|
|
// Check if the path is already in PATH
|
|
if strings.Contains(currentPath, wkhtmlPath) {
|
|
return nil // Already in PATH
|
|
}
|
|
|
|
// Add to PATH using OS-specific path separator
|
|
newPath := currentPath + pathSeparator + wkhtmlPath
|
|
if err := os.Setenv("PATH", newPath); err != nil {
|
|
return fmt.Errorf("failed to update PATH: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Added wkhtmltopdf to PATH: %s\n", wkhtmlPath)
|
|
return nil
|
|
}
|
|
|
|
// Add this helper function to get the executable name based on OS
|
|
func getExecutableName() string {
|
|
if runtime.GOOS == Windows {
|
|
return "wkhtmltopdf.exe"
|
|
}
|
|
return "wkhtmltopdf"
|
|
}
|
|
|
|
func main() {
|
|
conv := &Converter{}
|
|
|
|
// Parse command line flags
|
|
flag.StringVar(&conv.inputDir, "dir", ".", "Directory to scan for markdown files")
|
|
flag.BoolVar(&conv.recursive, "recursive", true, "Scan directories recursively")
|
|
flag.BoolVar(&conv.parallel, "parallel", true, "Convert files in parallel")
|
|
flag.BoolVar(&conv.hookMode, "hook", false, "Run as git pre-commit hook")
|
|
flag.Parse()
|
|
|
|
if conv.hookMode {
|
|
if err := conv.runAsHook(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Hook error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
if err := conv.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Converter) runAsHook() error {
|
|
// Ask user if they want to proceed
|
|
if !c.confirmConversion() {
|
|
fmt.Println("Skipping PDF conversion")
|
|
return nil
|
|
}
|
|
|
|
// Get staged markdown files
|
|
files, err := c.getStagedMarkdownFiles()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get staged files: %w", err)
|
|
}
|
|
|
|
if len(files) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Convert files
|
|
if err := c.convertFiles(files); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Stage generated PDFs
|
|
return c.stageGeneratedPDFs(files)
|
|
}
|
|
|
|
func (c *Converter) confirmConversion() bool {
|
|
fmt.Print("Convert Markdown files to PDF? [Y/n] ")
|
|
reader := bufio.NewReader(os.Stdin)
|
|
response, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
response = strings.ToLower(strings.TrimSpace(response))
|
|
return response == "" || response == "y" || response == "yes"
|
|
}
|
|
|
|
func (c *Converter) getStagedMarkdownFiles() ([]string, error) {
|
|
cmd := exec.Command("git", "diff", "--cached", "--name-only", "--diff-filter=d")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var files []string
|
|
scanner := bufio.NewScanner(bytes.NewReader(output))
|
|
for scanner.Scan() {
|
|
file := scanner.Text()
|
|
if strings.HasSuffix(file, ".md") || strings.HasSuffix(file, ".markdown") {
|
|
files = append(files, file)
|
|
}
|
|
}
|
|
|
|
return files, scanner.Err()
|
|
}
|
|
|
|
func (c *Converter) stageGeneratedPDFs(files []string) error {
|
|
for _, file := range files {
|
|
pdfFile := strings.TrimSuffix(file, filepath.Ext(file)) + ".pdf"
|
|
// Convert to OS-specific path
|
|
pdfFile = filepath.FromSlash(pdfFile)
|
|
cmd := exec.Command("git", "add", pdfFile)
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Printf("Warning: Could not stage %s\n", pdfFile)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Converter) Run() error {
|
|
files, err := c.findMarkdownFiles()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find markdown files: %w", err)
|
|
}
|
|
|
|
if len(files) == 0 {
|
|
fmt.Println("No markdown files found")
|
|
return nil
|
|
}
|
|
|
|
return c.convertFiles(files)
|
|
}
|
|
|
|
func (c *Converter) convertFiles(files []string) error {
|
|
if c.parallel {
|
|
return c.convertFilesParallel(files)
|
|
}
|
|
return c.convertFilesSerial(files)
|
|
}
|
|
|
|
func (c *Converter) findMarkdownFiles() ([]string, error) {
|
|
var files []string
|
|
|
|
walkFn := func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if info.IsDir() && !c.recursive && path != c.inputDir {
|
|
return filepath.SkipDir
|
|
}
|
|
|
|
if !info.IsDir() && (strings.HasSuffix(path, ".md") || strings.HasSuffix(path, ".markdown")) {
|
|
files = append(files, path)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if err := filepath.Walk(c.inputDir, walkFn); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return files, nil
|
|
}
|
|
|
|
func (c *Converter) convertFilesParallel(files []string) error {
|
|
var wg sync.WaitGroup
|
|
errors := make(chan error, len(files))
|
|
|
|
for _, file := range files {
|
|
wg.Add(1)
|
|
go func(f string) {
|
|
defer wg.Done()
|
|
if err := c.convertFile(f); err != nil {
|
|
errors <- fmt.Errorf("failed to convert %s: %w", f, err)
|
|
}
|
|
}(file)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errors)
|
|
|
|
for err := range errors {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Converter) convertFilesSerial(files []string) error {
|
|
for _, file := range files {
|
|
if err := c.convertFile(file); err != nil {
|
|
return fmt.Errorf("failed to convert %s: %w", file, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Converter) convertFile(inputFile string) error {
|
|
mdContent, err := os.ReadFile(inputFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read file: %w", err)
|
|
}
|
|
|
|
extensions := parser.CommonExtensions | parser.AutoHeadingIDs
|
|
p := parser.NewWithExtensions(extensions)
|
|
html := markdown.ToHTML(mdContent, p, nil)
|
|
|
|
outputFile := strings.TrimSuffix(inputFile, filepath.Ext(inputFile)) + ".pdf"
|
|
|
|
pdfg, err := wkhtmltopdf.NewPDFGenerator()
|
|
if err != nil {
|
|
if runtime.GOOS == Windows {
|
|
// Check if wkhtmltopdf exists in the expected path
|
|
expectedPath := filepath.Join(`C:\Program Files\wkhtmltopdf\bin`, "wkhtmltopdf.exe")
|
|
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
|
|
return fmt.Errorf("wkhtmltopdf not found at %s. Please ensure it's installed correctly", expectedPath)
|
|
}
|
|
}
|
|
return fmt.Errorf("failed to create PDF generator: %w", err)
|
|
}
|
|
page := wkhtmltopdf.NewPageReader(strings.NewReader(string(html)))
|
|
page.EnableLocalFileAccess.Set(true)
|
|
pdfg.AddPage(page)
|
|
|
|
pdfg.Dpi.Set(300)
|
|
pdfg.MarginTop.Set(15)
|
|
pdfg.MarginBottom.Set(15)
|
|
pdfg.MarginLeft.Set(15)
|
|
pdfg.MarginRight.Set(15)
|
|
|
|
if err := pdfg.Create(); err != nil {
|
|
return fmt.Errorf("failed to create PDF: %w", err)
|
|
}
|
|
|
|
if err := pdfg.WriteFile(outputFile); err != nil {
|
|
return fmt.Errorf("failed to write PDF file: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Successfully converted %s to %s\n", inputFile, outputFile)
|
|
return nil
|
|
}
|