Port from pv repo

This commit is contained in:
Warky 2024-12-07 11:46:52 +02:00
parent 90a01c63b9
commit 11513f541b
9 changed files with 469 additions and 1 deletions

31
.github/workflows/ci.yaml vendored Normal file
View File

@ -0,0 +1,31 @@
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Install dependencies
run: go mod download
- name: Run tests
run: go test -v ./...
- name: Run linting
uses: golangci/golangci-lint-action@v4
with:
version: latest

29
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

0
.goreleaser.yml Normal file
View File

123
README.md
View File

@ -1,2 +1,123 @@
# go-mdtopdf-helper
MD to PDF helper in Go
A command-line tool that automatically converts Markdown files to PDF using wkhtmltopdf. Perfect for maintaining PDF documentation alongside your Markdown files in Git repositories.
## Features
- Converts Markdown files to high-quality PDFs
- Can run as a Git pre-commit hook
- Recursive directory scanning
- Parallel file processing
- Cross-platform support (Windows, Linux, macOS)
- Automatic detection of wkhtmltopdf installation
## Prerequisites
This tool requires [wkhtmltopdf](https://wkhtmltopdf.org/) to be installed on your system. The application will check for its presence in standard installation locations:
- Windows: `C:\Program Files\wkhtmltopdf\bin`
- Linux: `/usr/local/bin/wkhtmltopdf` or `/usr/bin/wkhtmltopdf`
- macOS: `/usr/local/bin/wkhtmltopdf` or via Homebrew
If wkhtmltopdf is not found, you will be prompted to install it.
## Installation
```bash
go get github.com/Warky-Devs/go-mdtopdf-helper
```
## Usage
### Basic Usage
Convert Markdown files in the current directory:
```bash
go-mdtopdf-helper
```
### Command Line Options
```bash
go-mdtopdf-helper [options]
Options:
-dir string
Directory to scan for markdown files (default ".")
-recursive
Scan directories recursively (default true)
-parallel
Convert files in parallel (default true)
-hook
Run as git pre-commit hook
```
### Git Pre-commit Hook
To use as a Git pre-commit hook:
1. Create a file named `pre-commit` in your repository's `.git/hooks/` directory
2. Add the following content:
```bash
#!/bin/sh
go-mdtopdf-helper -hook
```
3. Make the hook executable:
```bash
chmod +x .git/hooks/pre-commit
```
When enabled as a pre-commit hook, the tool will:
1. Detect staged Markdown files
2. Ask for confirmation before conversion
3. Convert files to PDF
4. Automatically stage the generated PDFs
## PDF Output Configuration
The generated PDFs are configured with:
- 300 DPI resolution
- 15mm margins on all sides
- Local file access enabled for images
- Support for common Markdown extensions
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request. Here's how you can contribute:
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
### Development Setup
1. Clone the repository
2. Install dependencies:
```bash
go mod download
```
3. Make your changes
4. Run tests:
```bash
go test ./...
```
## Dependencies
- [go-wkhtmltopdf](https://github.com/SebastiaanKlippert/go-wkhtmltopdf) - Go wrapper for wkhtmltopdf
- [gomarkdown](https://github.com/gomarkdown/markdown) - Markdown parser and HTML renderer
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Acknowledgments
- Thanks to [SebastiaanKlippert](https://github.com/SebastiaanKlippert) for the go-wkhtmltopdf library
- Thanks to the gomarkdown team for their Markdown parser

BIN
README.pdf Normal file

Binary file not shown.

View File

@ -0,0 +1,2 @@
#!/bin/sh
go-mdtopdf-helper -hook

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module github.com/Warky-Devs/go-mdtopdf-helper.git
go 1.22.5
require (
github.com/SebastiaanKlippert/go-wkhtmltopdf v1.9.3
github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62
)

12
go.sum Normal file
View File

@ -0,0 +1,12 @@
github.com/SebastiaanKlippert/go-wkhtmltopdf v1.9.3 h1:vrA6+R1BMLKMTbos8jAeuBrImHPGtY4gTlcue3OIej8=
github.com/SebastiaanKlippert/go-wkhtmltopdf v1.9.3/go.mod h1:SQq4xfIdvf6WYKSDxAJc+xOJdolt+/bc1jnQKMtPMvQ=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62 h1:pbAFUZisjG4s6sxvRJvf2N7vhpCvx2Oxb3PmS6pDO1g=
github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

265
main.go Normal file
View File

@ -0,0 +1,265 @@
package main
import (
"bufio"
"bytes"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"github.com/SebastiaanKlippert/go-wkhtmltopdf"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/parser"
)
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 {
wkhtmlPath := `C:\Program Files\wkhtmltopdf\bin`
if _, err := os.Stat(wkhtmlPath); os.IsNotExist(err) {
return fmt.Errorf("wkhtmltopdf directory not found at: %s", wkhtmlPath)
}
currentPath := os.Getenv("PATH")
if strings.Contains(currentPath, wkhtmlPath) {
return nil // Already in PATH
}
newPath := currentPath + ";" + 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
}
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"
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 {
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
}