Documentation
¶
Overview ¶
Package picoloom converts Markdown documents to PDF using headless Chrome.
Quick Start ¶
Create a converter, convert markdown, and close when done:
conv, err := picoloom.NewConverter()
if err != nil {
log.Fatal(err)
}
defer conv.Close()
result, err := conv.Convert(ctx, picoloom.Input{
Markdown: "# Hello\n\nWorld",
})
if err != nil {
log.Fatal(err)
}
os.WriteFile("output.pdf", result.PDF, 0644)
The result contains both the PDF bytes (result.PDF) and the intermediate HTML (result.HTML) for debugging. Use Input.HTMLOnly to skip PDF generation.
Conversion Pipeline ¶
The conversion process follows these stages:
- Markdown preprocessing (line normalization, ==highlight== syntax)
- Markdown to HTML conversion via Goldmark (GFM, syntax highlighting)
- HTML injection (CSS, cover page, TOC, signature block)
- PDF rendering via headless Chrome (go-rod)
Configuration ¶
Use functional options to customize the converter:
conv, err := picoloom.NewConverter(
picoloom.WithTimeout(2 * time.Minute),
picoloom.WithStyle("technical"),
picoloom.WithAssetPath("/path/to/custom/assets"),
)
Per-conversion options are passed via Input:
result, err := conv.Convert(ctx, picoloom.Input{
Markdown: content,
SourceDir: "/path/to/markdown", // for relative image paths
CSS: "body { font-size: 14px; }",
Page: &picoloom.PageSettings{Size: "a4"},
Footer: &picoloom.Footer{ShowPageNumber: true},
Cover: &picoloom.Cover{Title: "Report"},
TOC: &picoloom.TOC{Title: "Contents"},
Watermark: &picoloom.Watermark{Text: "DRAFT"},
Signature: &picoloom.Signature{Name: "John Doe"},
})
Parallel Processing ¶
For batch conversion, use ConverterPool to manage multiple browser instances:
pool := picoloom.NewConverterPool(4) defer pool.Close() conv := pool.Acquire() defer pool.Release(conv) result, err := conv.Convert(ctx, input)
Custom Assets ¶
Override built-in styles and templates using AssetLoader:
loader, err := picoloom.NewAssetLoader("/path/to/assets")
conv, err := picoloom.NewConverter(picoloom.WithAssetLoader(loader))
Asset directory structure:
assets/
├── styles/
│ └── custom.css
└── templates/
└── custom/
├── cover.html
└── signature.html
Browser Requirements ¶
PDF generation requires Chrome/Chromium. The go-rod library automatically downloads a managed Chromium instance on first run (~/.cache/rod/browser/).
For containers and CI environments, set ROD_NO_SANDBOX=1 to disable the Chrome sandbox. Use ROD_BROWSER_BIN to specify a custom Chrome binary.
Example ¶
Example demonstrates basic markdown to HTML conversion. For PDF output, set HTMLOnly to false (requires Chrome).
conv, err := picoloom.NewConverter()
if err != nil {
fmt.Println("error:", err)
return
}
defer conv.Close()
result, err := conv.Convert(context.Background(), picoloom.Input{
Markdown: "# Hello World\n\nThis is a test.",
HTMLOnly: true, // Skip PDF generation for this example
})
if err != nil {
fmt.Println("error:", err)
return
}
// Check that HTML was generated
if strings.Contains(string(result.HTML), "<h1") {
fmt.Println("HTML generated successfully")
}
Output: HTML generated successfully
Example (WithCover) ¶
Example_withCover demonstrates adding a cover page.
conv, err := picoloom.NewConverter()
if err != nil {
fmt.Println("error:", err)
return
}
defer conv.Close()
result, err := conv.Convert(context.Background(), picoloom.Input{
Markdown: "# Introduction\n\nDocument content here.",
Cover: &picoloom.Cover{
Title: "Project Report",
Subtitle: "Q4 2025 Analysis",
Author: "John Doe",
Organization: "Acme Corp",
Date: "2025-12-15",
Version: "v1.0",
},
HTMLOnly: true,
})
if err != nil {
fmt.Println("error:", err)
return
}
if strings.Contains(string(result.HTML), "Project Report") {
fmt.Println("Cover page generated")
}
Output: Cover page generated
Example (WithCustomCSS) ¶
Example_withCustomCSS demonstrates injecting custom CSS.
conv, err := picoloom.NewConverter()
if err != nil {
fmt.Println("error:", err)
return
}
defer conv.Close()
result, err := conv.Convert(context.Background(), picoloom.Input{
Markdown: "# Styled Document\n\nCustom styling applied.",
CSS: `
body { font-family: Georgia, serif; }
h1 { color: #2c3e50; border-bottom: 2px solid #3498db; }
`,
HTMLOnly: true,
})
if err != nil {
fmt.Println("error:", err)
return
}
if strings.Contains(string(result.HTML), "Georgia") {
fmt.Println("Custom CSS injected")
}
Output: Custom CSS injected
Example (WithPageSettings) ¶
Example_withPageSettings demonstrates configuring page settings.
conv, err := picoloom.NewConverter()
if err != nil {
fmt.Println("error:", err)
return
}
defer conv.Close()
result, err := conv.Convert(context.Background(), picoloom.Input{
Markdown: "# A4 Document\n\nConfigured for A4 paper.",
Page: &picoloom.PageSettings{
Size: picoloom.PageSizeA4,
Orientation: picoloom.OrientationPortrait,
Margin: 1.0, // inches
},
HTMLOnly: true,
})
if err != nil {
fmt.Println("error:", err)
return
}
if len(result.HTML) > 0 {
fmt.Println("Page settings configured")
}
Output: Page settings configured
Example (WithSignature) ¶
Example_withSignature demonstrates adding a signature block.
conv, err := picoloom.NewConverter()
if err != nil {
fmt.Println("error:", err)
return
}
defer conv.Close()
result, err := conv.Convert(context.Background(), picoloom.Input{
Markdown: "# Report\n\nDocument content.",
Signature: &picoloom.Signature{
Name: "Jane Smith",
Title: "Senior Engineer",
Email: "jane@example.com",
Organization: "Tech Corp",
},
HTMLOnly: true,
})
if err != nil {
fmt.Println("error:", err)
return
}
if strings.Contains(string(result.HTML), "Jane Smith") {
fmt.Println("Signature block added")
}
Output: Signature block added
Example (WithTOC) ¶
Example_withTOC demonstrates adding a table of contents.
conv, err := picoloom.NewConverter()
if err != nil {
fmt.Println("error:", err)
return
}
defer conv.Close()
markdown := `# Document Title
## Chapter 1
Content for chapter 1.
## Chapter 2
Content for chapter 2.
### Section 2.1
Subsection content.
`
result, err := conv.Convert(context.Background(), picoloom.Input{
Markdown: markdown,
TOC: &picoloom.TOC{
Title: "Contents",
MinDepth: 2, // Start at h2 (skip document title)
MaxDepth: 3, // Include up to h3
},
HTMLOnly: true,
})
if err != nil {
fmt.Println("error:", err)
return
}
if strings.Contains(string(result.HTML), "toc") {
fmt.Println("TOC generated")
}
Output: TOC generated
Example (WithWatermark) ¶
Example_withWatermark demonstrates adding a watermark.
conv, err := picoloom.NewConverter()
if err != nil {
fmt.Println("error:", err)
return
}
defer conv.Close()
result, err := conv.Convert(context.Background(), picoloom.Input{
Markdown: "# Draft Document\n\nThis is a draft.",
Watermark: &picoloom.Watermark{
Text: "DRAFT",
Color: "#888888",
Opacity: 0.1,
Angle: -45,
},
HTMLOnly: true,
})
if err != nil {
fmt.Println("error:", err)
return
}
if strings.Contains(string(result.HTML), "DRAFT") {
fmt.Println("Watermark CSS generated")
}
Output: Watermark CSS generated
Index ¶
- Constants
- Variables
- func ResolvePoolSize(workers int) int
- type AssetLoader
- type ConvertResult
- type Converter
- type ConverterPool
- type Cover
- type Footer
- type Input
- type Link
- type Option
- type PageBreaks
- type PageSettings
- type Servicedeprecated
- type ServicePooldeprecated
- type Signature
- type TOC
- type TemplateSet
- type Watermark
Examples ¶
Constants ¶
const ( // DefaultStyle is the name of the built-in CSS style. DefaultStyle = "default" // DefaultTemplateSet is the name of the built-in template set. DefaultTemplateSet = "default" )
Asset name constants for built-in styles and templates.
const ( // MinPoolSize ensures at least one worker is available. MinPoolSize = 1 // MaxPoolSize caps browser instances to limit memory (~200MB each). MaxPoolSize = 8 )
Pool sizing constants.
const ( PageSizeLetter = "letter" PageSizeA4 = "a4" PageSizeLegal = "legal" )
Page size constants.
const ( OrientationPortrait = "portrait" OrientationLandscape = "landscape" )
Orientation constants.
const ( MinMargin = 0.25 MaxMargin = 3.0 DefaultMargin = 0.5 )
Margin bounds in inches.
const ( MinOrphans = 1 MaxOrphans = 5 DefaultOrphans = 2 MinWidows = 1 MaxWidows = 5 DefaultWidows = 2 )
Orphan/widow bounds for page break control.
const ( MinWatermarkOpacity = 0.0 MaxWatermarkOpacity = 1.0 DefaultWatermarkOpacity = 0.1 MinWatermarkAngle = -90.0 MaxWatermarkAngle = 90.0 DefaultWatermarkAngle = -45.0 DefaultWatermarkColor = "#888888" )
Watermark bounds.
const ( DefaultTOCMinDepth = 2 // Skip H1 by default (document title) DefaultTOCMaxDepth = 3 )
TOC depth bounds.
Variables ¶
var ( ErrEmptyMarkdown = errors.New("markdown content cannot be empty") ErrHTMLConversion = errors.New("HTML conversion failed") ErrPDFGeneration = errors.New("PDF generation failed") ErrBrowserConnect = errors.New("failed to connect to browser") ErrPageCreate = errors.New("failed to create browser page") ErrPageLoad = errors.New("failed to load page") ErrSignatureRender = errors.New("signature template rendering failed") // Page settings validation errors. ErrInvalidPageSize = errors.New("invalid page size") ErrInvalidOrientation = errors.New("invalid orientation") ErrInvalidMargin = errors.New("invalid margin") ErrInvalidFooterPosition = errors.New("invalid footer position") // Watermark validation errors. ErrInvalidWatermarkColor = errors.New("invalid watermark color") // Cover validation errors. ErrCoverLogoNotFound = errors.New("cover logo file not found") ErrCoverRender = errors.New("cover template rendering failed") // Signature validation errors. ErrSignatureImageNotFound = errors.New("signature image file not found") // TOC validation errors. ErrInvalidTOCDepth = errors.New("invalid TOC depth") // Page breaks validation errors. ErrInvalidOrphans = errors.New("invalid orphans value") ErrInvalidWidows = errors.New("invalid widows value") // Asset loading errors. ErrStyleNotFound = errors.New("style not found") ErrTemplateSetNotFound = errors.New("template set not found") ErrIncompleteTemplateSet = errors.New("template set missing required template") ErrInvalidAssetPath = errors.New("invalid asset path") )
Sentinel errors for library operations.
Functions ¶
func ResolvePoolSize ¶
ResolvePoolSize determines the optimal pool size. Priority: explicit workers > GOMAXPROCS-based calculation. Exported for use by servers and CLIs.
Types ¶
type AssetLoader ¶
type AssetLoader interface {
// LoadStyle loads a CSS style by name (without .css extension).
// Returns ErrStyleNotFound if the style doesn't exist.
LoadStyle(name string) (string, error)
// LoadTemplateSet loads cover and signature templates by name.
// Returns ErrTemplateSetNotFound if the template set doesn't exist.
// Returns ErrIncompleteTemplateSet if required templates are missing.
LoadTemplateSet(name string) (*TemplateSet, error)
}
AssetLoader defines the contract for loading CSS styles and HTML templates. Implementations may load from filesystem, embedded assets, S3, database, etc.
The library provides NewAssetLoader() for filesystem-based loading with fallback to embedded defaults. Implement this interface for custom backends.
func NewAssetLoader ¶
func NewAssetLoader(basePath string) (AssetLoader, error)
NewAssetLoader creates an AssetLoader for the given base path. If basePath is empty, returns a loader using only embedded assets. If basePath is set, custom assets take precedence with fallback to embedded.
The basePath directory should contain:
- styles/{name}.css for CSS styles
- templates/{name}/cover.html and signature.html for template sets
Returns ErrInvalidAssetPath if basePath is set but not a valid, readable directory.
Example ¶
ExampleNewAssetLoader demonstrates loading custom assets.
// NewAssetLoader with empty path uses embedded assets only
loader, err := picoloom.NewAssetLoader("")
if err != nil {
fmt.Println("error:", err)
return
}
conv, err := picoloom.NewConverter(picoloom.WithAssetLoader(loader))
if err != nil {
fmt.Println("error:", err)
return
}
defer conv.Close()
result, err := conv.Convert(context.Background(), picoloom.Input{
Markdown: "# Custom Assets\n\nUsing asset loader.",
HTMLOnly: true,
})
if err != nil {
fmt.Println("error:", err)
return
}
if len(result.HTML) > 0 {
fmt.Println("Asset loader configured")
}
Output: Asset loader configured
type ConvertResult ¶
type ConvertResult struct {
HTML []byte // Final HTML after all injections
PDF []byte // Generated PDF (empty if HTMLOnly)
}
ConvertResult holds both HTML and PDF output from conversion. HTML is always populated; PDF is empty when Input.HTMLOnly is true.
type Converter ¶
type Converter struct {
// contains filtered or unexported fields
}
Converter orchestrates the markdown-to-PDF conversion pipeline. Create with New() or NewConverter(), use Convert() for conversion, and Close() when done.
func New
deprecated
New creates a Converter with default configuration. Use options to customize behavior (e.g., WithTimeout, WithAssetLoader, WithTemplateSet). Returns error if asset loading or template parsing fails.
Deprecated: Use NewConverter instead. New will be removed in v2.
func NewConverter ¶
NewConverter creates a Converter with default configuration. Use options to customize behavior (e.g., WithTimeout, WithAssetLoader, WithTemplateSet). Returns error if asset loading or template parsing fails.
Example (WithStyle) ¶
ExampleNewConverter_withStyle demonstrates using a built-in style.
conv, err := picoloom.NewConverter(picoloom.WithStyle("technical"))
if err != nil {
fmt.Println("error:", err)
return
}
defer conv.Close()
result, err := conv.Convert(context.Background(), picoloom.Input{
Markdown: "# Technical Document\n\nUsing the technical style.",
HTMLOnly: true,
})
if err != nil {
fmt.Println("error:", err)
return
}
// Technical style uses system-ui font
if strings.Contains(string(result.HTML), "system-ui") {
fmt.Println("Technical style applied")
}
Output: Technical style applied
func (*Converter) Convert ¶
Convert runs the full pipeline and returns the result containing HTML and PDF. The context is used for cancellation and timeout. If input.HTMLOnly is true, PDF generation is skipped (for debugging). Recovers from internal panics to prevent crashes from propagating to callers.
type ConverterPool ¶
type ConverterPool struct {
// contains filtered or unexported fields
}
ConverterPool manages a pool of Converter instances for parallel processing. Each converter has its own browser instance, enabling true parallelism. Converters are created lazily on first acquire to avoid startup delay.
Example ¶
ExampleConverterPool demonstrates parallel batch processing.
pool := picoloom.NewConverterPool(2)
// Process two documents in parallel
docs := []string{
"# Document 1\n\nFirst document.",
"# Document 2\n\nSecond document.",
}
// Channel to collect results, WaitGroup to synchronize goroutines
results := make(chan bool, len(docs))
var wg sync.WaitGroup
for _, doc := range docs {
wg.Add(1)
go func(markdown string) {
defer wg.Done()
conv := pool.Acquire()
if conv == nil {
results <- false
return
}
defer pool.Release(conv)
result, err := conv.Convert(context.Background(), picoloom.Input{
Markdown: markdown,
HTMLOnly: true,
})
results <- err == nil && strings.Contains(string(result.HTML), "Document")
}(doc)
}
// Wait for all goroutines to finish before closing pool
wg.Wait()
pool.Close()
// Collect results
success := 0
for range docs {
if <-results {
success++
}
}
fmt.Printf("Processed %d documents\n", success)
Output: Processed 2 documents
func NewConverterPool ¶
func NewConverterPool(n int, opts ...Option) *ConverterPool
NewConverterPool creates a pool with capacity for n Converter instances. Converters are created lazily when acquired, not at pool creation. Options are applied to each converter when created.
func NewServicePool
deprecated
func NewServicePool(n int, opts ...Option) *ConverterPool
NewServicePool creates a pool with capacity for n Converter instances.
Deprecated: Use NewConverterPool instead. NewServicePool will be removed in v2.
func (*ConverterPool) Acquire ¶
func (p *ConverterPool) Acquire() *Converter
Acquire gets a converter from the pool, creating one if needed. Blocks if all converters are in use. Returns nil and sets internal error if converter creation fails. Use InitError() to check for initialization failures.
func (*ConverterPool) Close ¶
func (p *ConverterPool) Close() error
Close releases all browser resources. Returns an aggregated error if multiple converters fail to close.
func (*ConverterPool) InitError ¶
func (p *ConverterPool) InitError() error
InitError returns the first error encountered during converter creation. Returns nil if all converters were created successfully.
func (*ConverterPool) Release ¶
func (p *ConverterPool) Release(conv *Converter)
Release returns a converter to the pool. The lock is released before sending to avoid deadlock when channel is full.
type Cover ¶
type Cover struct {
Title string // Document title (required)
Subtitle string // Optional subtitle
Logo string // Logo path or URL (optional)
Author string // Author name (optional)
AuthorTitle string // Author's professional title (optional)
Organization string // Organization name (optional)
Date string // Date string (optional)
Version string // Version string (optional)
// Extended metadata fields
ClientName string // Client/customer name (optional)
ProjectName string // Project name (optional)
DocumentType string // Document type, e.g., "Technical Specification" (optional)
DocumentID string // Document reference, e.g., "DOC-2024-001" (optional)
Description string // Brief document summary (optional)
Department string // Author's department (optional, shared with Signature via config)
}
Cover configures the cover page.
type Input ¶
type Input struct {
Markdown string // Markdown content (required)
SourceDir string // Base directory for resolving relative paths (optional)
CSS string // Custom CSS (optional)
Signature *Signature // Signature config (optional)
Page *PageSettings // Page settings (optional, nil = defaults)
Watermark *Watermark // Watermark config (optional)
Cover *Cover // Cover page config (optional)
TOC *TOC // Table of contents config (optional)
PageBreaks *PageBreaks // Page break config (optional)
HTMLOnly bool // If true, skip PDF generation (for debugging)
}
Input contains conversion parameters.
type Option ¶
type Option func(*Converter)
Option configures a Converter.
func WithAssetLoader ¶
func WithAssetLoader(loader AssetLoader) Option
WithAssetLoader sets a custom asset loader for CSS styles and HTML templates. Use NewAssetLoader(basePath) to load from a custom directory with fallback to embedded assets, or implement AssetLoader for custom backends.
Example:
loader, err := picoloom.NewAssetLoader("/path/to/assets")
if err != nil {
log.Fatal(err)
}
conv, err := picoloom.NewConverter(picoloom.WithAssetLoader(loader))
func WithAssetPath ¶
WithAssetPath configures asset loading from a filesystem directory. Custom assets take precedence; missing assets fall back to embedded defaults.
The directory should contain:
- styles/{name}.css for CSS styles
- templates/{name}/cover.html and signature.html for template sets
This is equivalent to calling NewAssetLoader(path) and WithAssetLoader(). Returns error from NewConverter() if the path is invalid.
func WithStyle ¶
WithStyle sets the CSS style for all conversions. Accepts:
- Style name: "technical", "default", "corporate"
- File path: "./custom.css", "/path/to/style.css"
- CSS content: "body { font-size: 14px; }"
Detection: paths contain / or \, CSS content contains {, otherwise treated as a style name.
func WithTemplateSet ¶
func WithTemplateSet(ts *TemplateSet) Option
WithTemplateSet sets a custom template set for cover and signature. Use this to override the default templates loaded from embedded assets.
Example:
ts := picoloom.NewTemplateSet("custom", coverHTML, signatureHTML)
conv, err := picoloom.NewConverter(picoloom.WithTemplateSet(ts))
func WithTimeout ¶
WithTimeout sets the conversion timeout. Panics if d <= 0 (programmer error, similar to time.NewTicker).
type PageBreaks ¶
type PageBreaks struct {
BeforeH1 bool // Page break before H1 headings (default: false)
BeforeH2 bool // Page break before H2 headings (default: false)
BeforeH3 bool // Page break before H3 headings (default: false)
Orphans int // Min lines at page bottom (default: 2, range: 1-5)
Widows int // Min lines at page top (default: 2, range: 1-5)
}
PageBreaks configures page break behavior for PDF output.
func (*PageBreaks) Validate ¶
func (pb *PageBreaks) Validate() error
Validate checks that page break settings are valid. Returns nil if pb is nil (nil means use defaults).
type PageSettings ¶
type PageSettings struct {
Size string // "letter", "a4", "legal"
Orientation string // "portrait", "landscape"
Margin float64 // inches, applied to all sides
}
PageSettings configures PDF page dimensions.
func DefaultPageSettings ¶
func DefaultPageSettings() *PageSettings
DefaultPageSettings returns page settings with default values.
func (*PageSettings) Validate ¶
func (p *PageSettings) Validate() error
Validate checks that page settings are valid. Returns nil if p is nil (nil means use defaults). Empty values are allowed and will use defaults at runtime. Does not mutate - uses case-insensitive comparison.
type ServicePool
deprecated
type ServicePool = ConverterPool
ServicePool is an alias for ConverterPool for backward compatibility.
Deprecated: Use ConverterPool instead. This alias will be removed in v2.
type Signature ¶
type Signature struct {
Name string
Title string
Email string
Organization string
ImagePath string
Links []Link
// Extended metadata fields
Phone string // Contact phone number (optional)
Address string // Postal address (optional, use YAML literal block for multiline)
Department string // Department name (optional)
}
Signature configures the signature block.
func (*Signature) Validate ¶
Validate checks that signature settings are valid. Returns nil if s is nil (nil means no signature).
Note: Only ImagePath is validated (file existence). Other fields like Email and Links are pure content that renders as-is - this is a PDF rendering tool, not a data validation tool. Users control their content.
type TOC ¶
type TOC struct {
Title string // Title above TOC (empty = no title)
MinDepth int // 1-6, minimum heading level to include (default: 2, skips H1)
MaxDepth int // 1-6, maximum heading level to include (default: 3)
}
TOC configures the table of contents.
type TemplateSet ¶
type TemplateSet struct {
Name string // Identifier (name or path)
Cover string // Cover page template HTML
Signature string // Signature block template HTML
}
TemplateSet holds HTML templates for document generation. A template set contains cover and signature templates that work together.
func NewTemplateSet ¶
func NewTemplateSet(name, cover, signature string) *TemplateSet
NewTemplateSet creates a TemplateSet from cover and signature HTML content. This is a convenience constructor for users providing templates directly.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
picoloom
command
Package main provides the md2pdf CLI.
|
Package main provides the md2pdf CLI. |
|
picoloom-migrate
command
Command picoloom-migrate rewrites common go-md2pdf references to picoloom/v2.
|
Command picoloom-migrate rewrites common go-md2pdf references to picoloom/v2. |
|
internal
|
|
|
assets
Package assets provides CSS styles and HTML templates for PDF generation.
|
Package assets provides CSS styles and HTML templates for PDF generation. |
|
config
Package config centralizes parsing and validation so every entry point enforces the same safety and defaulting rules.
|
Package config centralizes parsing and validation so every entry point enforces the same safety and defaulting rules. |
|
dateutil
Package dateutil provides date format parsing utilities.
|
Package dateutil provides date format parsing utilities. |
|
fileutil
Package fileutil provides file and path utility functions.
|
Package fileutil provides file and path utility functions. |
|
hints
Package hints provides actionable error hints for common failure scenarios.
|
Package hints provides actionable error hints for common failure scenarios. |
|
pipeline
Package pipeline implements the Markdown-to-HTML conversion pipeline.
|
Package pipeline implements the Markdown-to-HTML conversion pipeline. |
|
process
Package process contains process-control helpers used for robust shutdown.
|
Package process contains process-control helpers used for robust shutdown. |
|
styleinput
Package styleinput classifies user-provided style values so callers can resolve style names, files, and inline CSS through one decision path.
|
Package styleinput classifies user-provided style values so callers can resolve style names, files, and inline CSS through one decision path. |
|
yamlutil
Package yamlutil wraps YAML parsing to isolate the external dependency.
|
Package yamlutil wraps YAML parsing to isolate the external dependency. |
