diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index feadff73..5b232ce0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,3 +79,10 @@ jobs: - name: Test docs examples run: go test ./assets/examples/... working-directory: docs + + # docs/assets/js/paper-fs.js hand-implements the filesystem syscalls Go's + # js/wasm runtime calls. A regression there does not fail loudly — the + # renderer draws an error box and still emits a valid PDF — so it is + # pinned by its own tests. Node is preinstalled on ubuntu-latest. + - name: Test browser filesystem shim + run: node --test docs/assets/js/paper-fs.test.mjs diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 0f554755..2a47c2db 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -46,14 +46,20 @@ jobs: cp -r docs _site rm -f _site/go.mod _site/go.sum rm -rf _site/plans - # Keep assets/examples/*/main.go — docsify embeds them via ':include'. + # Keep assets/examples/**/*.go — docsify embeds them via ':include'. find _site -name '*_test.go' -delete - mkdir -p _site/playground - cp examples/cmd/wasm/web/index.html _site/playground/ - cp examples/cmd/wasm/web/paper.wasm _site/playground/ - cp examples/cmd/wasm/web/wasm_exec.js _site/playground/ + # The docs pages generate their PDF previews in the browser, so the + # wasm has to be reachable from the site root as well as the + # playground. The playground keeps its own copy so that it stays + # servable standalone from examples/cmd/wasm/web. + mkdir -p _site/assets/wasm _site/playground + cp -f examples/cmd/wasm/web/paper.wasm _site/assets/wasm/ + cp -f examples/cmd/wasm/web/wasm_exec.js _site/assets/wasm/ + cp -f examples/cmd/wasm/web/index.html _site/playground/ + cp -f examples/cmd/wasm/web/paper.wasm _site/playground/ + cp -f examples/cmd/wasm/web/wasm_exec.js _site/playground/ touch _site/.nojekyll - echo "Assembled site:" && ls -1 _site && echo "playground:" && ls -1 _site/playground + echo "Assembled site:" && ls -1 _site && echo "wasm:" && ls -1 _site/assets/wasm && echo "playground:" && ls -1 _site/playground - name: Upload Pages artifact uses: actions/upload-pages-artifact@v3 diff --git a/.gitignore b/.gitignore index b2a6317b..e38dfb80 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,22 @@ docs/plans/ examples/cmd/wasm/web/paper.wasm examples/cmd/wasm/web/wasm_exec.js +# Copy of the wasm the docs site serves for its in-browser PDF previews +# (placed by `make docs` / `make site`; built, not authored) +docs/assets/wasm/ + +# Example PDFs are generated by `make examples` and rendered in the browser by +# paper.wasm, so they are no longer committed. Six survive: five whose inputs are +# impractical to ship to a browser and stay static embeds, plus paper.pdf, which +# the mergepdf example reads as an input. +docs/assets/pdf/*.pdf +!docs/assets/pdf/paper.pdf +!docs/assets/pdf/showcase.pdf +!docs/assets/pdf/background.pdf +!docs/assets/pdf/customfont.pdf +!docs/assets/pdf/mergepdf.pdf +!docs/assets/pdf/disablepagebreak.pdf + # playwright-cli E2E scratch output .playwright-cli/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 85176819..315262c9 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -79,8 +79,26 @@ Tests must create mocks via constructors (`m := mocks.NewProvider(t)`); ## Docs site ```bash -make docs # docsify serve docs/ on http://localhost:3000 +make docs # builds the wasm, copies it into docs/assets/wasm/, then + # docsify serve docs/ on http://localhost:3000 +make site # assembles the full Pages site into site/ and serves :8080 ``` +`make docs` depends on `make wasm` because the feature pages generate their PDF +previews in the browser from `docs/assets/wasm/paper.wasm`; without it every +preview renders an error box. That directory is generated and git-ignored. + +Example PDFs under `docs/assets/pdf/` are produced by `make examples` and are +**not** committed, apart from six: `background`, `customfont`, +`disablepagebreak`, `mergepdf` and `showcase` stay static embeds because their +inputs are impractical to ship to a browser, and `paper.pdf` is an input the +`mergepdf` example reads. Note that `paper.pdf` is generated by +`examples/cmd/dev/pdf`, not by `make examples`. + +Regenerating the tracked PDFs always produces a diff even with no code change, +because no example sets `WithDeterministic` and the embedded `CreationDate` +moves. Check the structure fixtures under `test/paper/examples/` for real +output changes. + `docs/plans/` holds internal planning documents and is excluded from version control and the site. diff --git a/Makefile b/Makefile index 8e8aaad3..01169e67 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,10 @@ test: go test $(GO_PATHS) cd examples && go test ./... cd docs && go test ./assets/examples/... + # The browser filesystem shim implements a Go syscall contract by hand, and a + # break in it renders an error box into an otherwise valid PDF rather than + # failing, so it gets its own tests. + node --test docs/assets/js/paper-fs.test.mjs .PHONY: fmt fmt: @@ -44,8 +48,13 @@ mock-lint: install: bash shell/install.sh +# Serve the docs locally. Depends on wasm because the feature pages generate +# their PDF previews in the browser from docs/assets/wasm/paper.wasm. .PHONY: docs -docs: +docs: wasm + mkdir -p docs/assets/wasm + # -f: the toolchain's wasm_exec.js is read-only (0444), so the copy is too. + cp -f examples/cmd/wasm/web/paper.wasm examples/cmd/wasm/web/wasm_exec.js docs/assets/wasm/ docsify serve docs/ .PHONY: godoc @@ -72,46 +81,49 @@ site: wasm cp -r docs site rm -f site/go.mod site/go.sum rm -rf site/plans - find site -name '*.go' -delete - mkdir -p site/playground - cp examples/cmd/wasm/web/index.html examples/cmd/wasm/web/paper.wasm examples/cmd/wasm/web/wasm_exec.js site/playground/ + # Only test files go: docsify ':include's the example sources, so deleting + # every *.go (as this used to) left the code samples on the site empty. + find site -name '*_test.go' -delete + mkdir -p site/assets/wasm site/playground + cp -f examples/cmd/wasm/web/paper.wasm examples/cmd/wasm/web/wasm_exec.js site/assets/wasm/ + cp -f examples/cmd/wasm/web/index.html examples/cmd/wasm/web/paper.wasm examples/cmd/wasm/web/wasm_exec.js site/playground/ @echo "Serving site at http://localhost:8080/ (playground: http://localhost:8080/playground/)" cd site && python3 -m http.server 8080 .PHONY: examples examples: - go run docs/assets/examples/addpage/main.go - go run docs/assets/examples/autorow/main.go - go run docs/assets/examples/background/main.go - go run docs/assets/examples/barcodegrid/main.go - go run docs/assets/examples/billing/main.go - go run docs/assets/examples/bookmark/main.go + go run ./docs/assets/examples/addpage/cmd + go run ./docs/assets/examples/autorow/cmd + go run ./docs/assets/examples/background/cmd + go run ./docs/assets/examples/barcodegrid/cmd + go run ./docs/assets/examples/billing/cmd + go run ./docs/assets/examples/bookmark/cmd cd examples && go run ./cmd/paper-showcase ../docs/assets/pdf/showcase.pdf - go run docs/assets/examples/cellstyle/main.go - go run docs/assets/examples/checkbox/main.go - go run docs/assets/examples/compression/main.go - go run docs/assets/examples/customdimensions/main.go - go run docs/assets/examples/customfont/main.go - go run docs/assets/examples/custompage/main.go - go run docs/assets/examples/datamatrixgrid/main.go - go run docs/assets/examples/disablepagebreak/main.go - go run docs/assets/examples/footer/main.go - go run docs/assets/examples/header/main.go - go run docs/assets/examples/imagegrid/main.go - go run docs/assets/examples/line/main.go - go run docs/assets/examples/list/main.go - go run docs/assets/examples/lowmemory/main.go - go run docs/assets/examples/margins/main.go - go run docs/assets/examples/maxgridsum/main.go - go run docs/assets/examples/mergepdf/main.go - go run docs/assets/examples/metadatas/main.go - go run docs/assets/examples/orientation/main.go - go run docs/assets/examples/pagenumber/main.go - go run docs/assets/examples/parallelism/main.go - go run docs/assets/examples/protection/main.go - go run docs/assets/examples/qrgrid/main.go - go run docs/assets/examples/signaturegrid/main.go - go run docs/assets/examples/simplest/main.go - go run docs/assets/examples/textgrid/main.go - go run docs/assets/examples/watermark/main.go + go run ./docs/assets/examples/cellstyle/cmd + go run ./docs/assets/examples/checkbox/cmd + go run ./docs/assets/examples/compression/cmd + go run ./docs/assets/examples/customdimensions/cmd + go run ./docs/assets/examples/customfont/cmd + go run ./docs/assets/examples/custompage/cmd + go run ./docs/assets/examples/datamatrixgrid/cmd + go run ./docs/assets/examples/disablepagebreak/cmd + go run ./docs/assets/examples/footer/cmd + go run ./docs/assets/examples/header/cmd + go run ./docs/assets/examples/imagegrid/cmd + go run ./docs/assets/examples/line/cmd + go run ./docs/assets/examples/list/cmd + go run ./docs/assets/examples/lowmemory/cmd + go run ./docs/assets/examples/margins/cmd + go run ./docs/assets/examples/maxgridsum/cmd + go run ./docs/assets/examples/mergepdf/cmd + go run ./docs/assets/examples/metadatas/cmd + go run ./docs/assets/examples/orientation/cmd + go run ./docs/assets/examples/pagenumber/cmd + go run ./docs/assets/examples/parallelism/cmd + go run ./docs/assets/examples/protection/cmd + go run ./docs/assets/examples/qrgrid/cmd + go run ./docs/assets/examples/signaturegrid/cmd + go run ./docs/assets/examples/simplest/cmd + go run ./docs/assets/examples/textgrid/cmd + go run ./docs/assets/examples/watermark/cmd go test docs/assets/examples/unittests/main_test.go diff --git a/docs/README.md b/docs/README.md index 77791cd4..5089a6c8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -42,7 +42,7 @@ Use the row/column API when you need to mix HTML with manual components, headers ## Code Example This is part of the [simplest example](examples/simplest?id=simplest). -[filename](assets/examples/simplest/main.go ':include :type=code') +[filename](assets/examples/simplest/paper.go ':include :type=code') ## PDF Example This is part of the [showcase example](examples/showcase?id=showcase). diff --git a/docs/assets/examples/addpage/cmd/main.go b/docs/assets/examples/addpage/cmd/main.go new file mode 100644 index 00000000..6ab924c6 --- /dev/null +++ b/docs/assets/examples/addpage/cmd/main.go @@ -0,0 +1,29 @@ +// Command addpage writes the addpage example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/addpage/cmd +package main + +import ( + "context" + "log" + + "github.com/avdoseferovic/paper/docs/assets/examples/addpage" +) + +func main() { + m := addpage.GetPaper() + + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/addpage.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/addpage.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/addpage/main.go b/docs/assets/examples/addpage/paper.go similarity index 66% rename from docs/assets/examples/addpage/main.go rename to docs/assets/examples/addpage/paper.go index 4643cae4..b849fe55 100644 --- a/docs/assets/examples/addpage/main.go +++ b/docs/assets/examples/addpage/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates adding pages explicitly, so content starts on a fresh page. -package main +// Package addpage demonstrates adding pages explicitly, so content starts on a fresh page. +package addpage import ( - "context" - "log" - "github.com/avdoseferovic/paper" "github.com/avdoseferovic/paper/pkg/components/page" "github.com/avdoseferovic/paper/pkg/components/text" @@ -13,25 +10,7 @@ import ( "github.com/avdoseferovic/paper/pkg/decorator" ) -func main() { - m := GetPaper() - - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/addpage.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/addpage.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the addpage example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithPageNumber(). diff --git a/docs/assets/examples/addpage/main_test.go b/docs/assets/examples/addpage/paper_test.go similarity index 93% rename from docs/assets/examples/addpage/main_test.go rename to docs/assets/examples/addpage/paper_test.go index 3bdec164..aba84419 100644 --- a/docs/assets/examples/addpage/main_test.go +++ b/docs/assets/examples/addpage/paper_test.go @@ -1,4 +1,4 @@ -package main +package addpage import ( "testing" diff --git a/docs/assets/examples/autorow/cmd/main.go b/docs/assets/examples/autorow/cmd/main.go new file mode 100644 index 00000000..c7f7aebe --- /dev/null +++ b/docs/assets/examples/autorow/cmd/main.go @@ -0,0 +1,28 @@ +// Command autorow writes the autorow example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/autorow/cmd +package main + +import ( + "context" + "log" + + "github.com/avdoseferovic/paper/docs/assets/examples/autorow" +) + +func main() { + m := autorow.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/autorow.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/autorow.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/autorow/main.go b/docs/assets/examples/autorow/paper.go similarity index 80% rename from docs/assets/examples/autorow/main.go rename to docs/assets/examples/autorow/paper.go index 69dd5865..942546da 100644 --- a/docs/assets/examples/autorow/main.go +++ b/docs/assets/examples/autorow/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates rows that take their height from the content inside them. -package main +// Package autorow demonstrates rows that take their height from the content inside them. +package autorow import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/components/code" "github.com/avdoseferovic/paper/pkg/components/image" "github.com/avdoseferovic/paper/pkg/components/text" @@ -18,24 +15,7 @@ import ( "github.com/avdoseferovic/paper/pkg/config" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/autorow.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/autorow.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the autorow example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithDebug(true). diff --git a/docs/assets/examples/autorow/main_test.go b/docs/assets/examples/autorow/paper_test.go similarity index 93% rename from docs/assets/examples/autorow/main_test.go rename to docs/assets/examples/autorow/paper_test.go index aeafada8..7e7aced0 100644 --- a/docs/assets/examples/autorow/main_test.go +++ b/docs/assets/examples/autorow/paper_test.go @@ -1,4 +1,4 @@ -package main +package autorow import ( "testing" diff --git a/docs/assets/examples/background/cmd/main.go b/docs/assets/examples/background/cmd/main.go new file mode 100644 index 00000000..226ae47e --- /dev/null +++ b/docs/assets/examples/background/cmd/main.go @@ -0,0 +1,29 @@ +// Command background writes the background example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/background/cmd +package main + +import ( + "context" + "log" + + "github.com/avdoseferovic/paper/docs/assets/examples/background" +) + +func main() { + backgroundImage := "docs/assets/images/certificate.png" + m := background.GetPaper(backgroundImage) + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/background.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/background.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/background/main.go b/docs/assets/examples/background/paper.go similarity index 73% rename from docs/assets/examples/background/main.go rename to docs/assets/examples/background/paper.go index e6f21ffe..676a1df8 100644 --- a/docs/assets/examples/background/main.go +++ b/docs/assets/examples/background/paper.go @@ -1,8 +1,7 @@ -// Package main demonstrates drawing a full-page background image behind the content. -package main +// Package background demonstrates drawing a full-page background image behind the content. +package background import ( - "context" "log" "os" @@ -23,25 +22,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - backgroundImage := "docs/assets/images/certificate.png" - m := GetPaper(backgroundImage) - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/background.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/background.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the background example document. func GetPaper(image string) core.Paper { bytes, err := os.ReadFile(image) if err != nil { @@ -62,6 +43,7 @@ func GetPaper(image string) core.Paper { return m } +// AddPage is part of the background example. func AddPage() core.Page { return page.New().Add( row.New(70), diff --git a/docs/assets/examples/background/main_test.go b/docs/assets/examples/background/paper_test.go similarity index 96% rename from docs/assets/examples/background/main_test.go rename to docs/assets/examples/background/paper_test.go index 7913721b..f4fa92f5 100644 --- a/docs/assets/examples/background/main_test.go +++ b/docs/assets/examples/background/paper_test.go @@ -1,4 +1,4 @@ -package main +package background import ( "os" diff --git a/docs/assets/examples/barcodegrid/cmd/main.go b/docs/assets/examples/barcodegrid/cmd/main.go new file mode 100644 index 00000000..e11bb9e1 --- /dev/null +++ b/docs/assets/examples/barcodegrid/cmd/main.go @@ -0,0 +1,28 @@ +// Command barcodegrid writes the barcodegrid example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/barcodegrid/cmd +package main + +import ( + "context" + "log" + + "github.com/avdoseferovic/paper/docs/assets/examples/barcodegrid" +) + +func main() { + m := barcodegrid.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/barcodegrid.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/barcodegrid.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/barcodegrid/main.go b/docs/assets/examples/barcodegrid/paper.go similarity index 84% rename from docs/assets/examples/barcodegrid/main.go rename to docs/assets/examples/barcodegrid/paper.go index 7a04139f..ddf8b05a 100644 --- a/docs/assets/examples/barcodegrid/main.go +++ b/docs/assets/examples/barcodegrid/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates placing Code 128 barcodes across a grid of columns. -package main +// Package barcodegrid demonstrates placing Code 128 barcodes across a grid of columns. +package barcodegrid import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/consts" "github.com/avdoseferovic/paper/pkg/core" @@ -17,24 +14,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/barcodegrid.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/barcodegrid.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the barcodegrid example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithDebug(true). diff --git a/docs/assets/examples/barcodegrid/main_test.go b/docs/assets/examples/barcodegrid/paper_test.go similarity index 92% rename from docs/assets/examples/barcodegrid/main_test.go rename to docs/assets/examples/barcodegrid/paper_test.go index 4bdee565..8bf39d67 100644 --- a/docs/assets/examples/barcodegrid/main_test.go +++ b/docs/assets/examples/barcodegrid/paper_test.go @@ -1,4 +1,4 @@ -package main +package barcodegrid import ( "testing" diff --git a/docs/assets/examples/billing/cmd/main.go b/docs/assets/examples/billing/cmd/main.go new file mode 100644 index 00000000..2cfe0777 --- /dev/null +++ b/docs/assets/examples/billing/cmd/main.go @@ -0,0 +1,28 @@ +// Command billing writes the billing example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/billing/cmd +package main + +import ( + "context" + "log" + + "github.com/avdoseferovic/paper/docs/assets/examples/billing" +) + +func main() { + m := billing.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/billing.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/billing.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/billing/main.go b/docs/assets/examples/billing/paper.go similarity index 92% rename from docs/assets/examples/billing/main.go rename to docs/assets/examples/billing/paper.go index 39687263..f82f6d53 100644 --- a/docs/assets/examples/billing/main.go +++ b/docs/assets/examples/billing/paper.go @@ -1,8 +1,7 @@ -// Package main demonstrates a complete invoice: header, line-item table, and totals. -package main +// Package billing demonstrates a complete invoice: header, line-item table, and totals. +package billing import ( - "context" "log" "github.com/avdoseferovic/paper" @@ -21,24 +20,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/billing.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/billing.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the billing example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithPageNumber(). diff --git a/docs/assets/examples/billing/main_test.go b/docs/assets/examples/billing/paper_test.go similarity index 93% rename from docs/assets/examples/billing/main_test.go rename to docs/assets/examples/billing/paper_test.go index d1e96310..e72686f0 100644 --- a/docs/assets/examples/billing/main_test.go +++ b/docs/assets/examples/billing/paper_test.go @@ -1,4 +1,4 @@ -package main +package billing import ( "testing" diff --git a/docs/assets/examples/bookmark/cmd/main.go b/docs/assets/examples/bookmark/cmd/main.go new file mode 100644 index 00000000..5179973a --- /dev/null +++ b/docs/assets/examples/bookmark/cmd/main.go @@ -0,0 +1,24 @@ +// Command bookmark writes the bookmark example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/bookmark/cmd +package main + +import ( + "context" + "log" + + "github.com/avdoseferovic/paper/docs/assets/examples/bookmark" +) + +func main() { + m := bookmark.GetPaper() + + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/bookmark.pdf") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/bookmark/main.go b/docs/assets/examples/bookmark/paper.go similarity index 73% rename from docs/assets/examples/bookmark/main.go rename to docs/assets/examples/bookmark/paper.go index e5562678..37f2d6f3 100644 --- a/docs/assets/examples/bookmark/main.go +++ b/docs/assets/examples/bookmark/paper.go @@ -1,18 +1,17 @@ -// Package main demonstrates PDF outline bookmarks: text components with an +// Package bookmark demonstrates PDF outline bookmarks: text components with an // Outline prop appear in the PDF viewer's bookmark sidebar. -package main +package bookmark import ( - "context" - "log" - "github.com/avdoseferovic/paper" "github.com/avdoseferovic/paper/pkg/components/col" "github.com/avdoseferovic/paper/pkg/components/text" + "github.com/avdoseferovic/paper/pkg/core" "github.com/avdoseferovic/paper/pkg/props" ) -func main() { +// GetPaper builds the bookmark example document. +func GetPaper() core.Paper { m := paper.New() m.AddAutoRow(col.New(12).Add(text.New("Introduction", props.Text{ @@ -34,13 +33,5 @@ func main() { Outline: &props.Outline{Level: 1, Title: "Your first document"}, }))) - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/bookmark.pdf") - if err != nil { - log.Fatal(err.Error()) - } + return m } diff --git a/docs/assets/examples/bookmark/paper_test.go b/docs/assets/examples/bookmark/paper_test.go new file mode 100644 index 00000000..ca5586da --- /dev/null +++ b/docs/assets/examples/bookmark/paper_test.go @@ -0,0 +1,16 @@ +package bookmark + +import ( + "testing" + + "github.com/avdoseferovic/paper/internal/test" +) + +func TestGetPaper(t *testing.T) { + t.Parallel() + // Act + sut := GetPaper() + + // Assert + test.New(t).Assert(sut.GetStructure()).Equals("examples/bookmark.json") +} diff --git a/docs/assets/examples/cellstyle/cmd/main.go b/docs/assets/examples/cellstyle/cmd/main.go new file mode 100644 index 00000000..0ddb0e29 --- /dev/null +++ b/docs/assets/examples/cellstyle/cmd/main.go @@ -0,0 +1,28 @@ +// Command cellstyle writes the cellstyle example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/cellstyle/cmd +package main + +import ( + "context" + "log" + + "github.com/avdoseferovic/paper/docs/assets/examples/cellstyle" +) + +func main() { + m := cellstyle.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/cellstyle.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/cellstyle.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/cellstyle/main.go b/docs/assets/examples/cellstyle/paper.go similarity index 87% rename from docs/assets/examples/cellstyle/main.go rename to docs/assets/examples/cellstyle/paper.go index a4bbbc82..7f7af052 100644 --- a/docs/assets/examples/cellstyle/main.go +++ b/docs/assets/examples/cellstyle/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates styling individual cells with fills, borders, and padding. -package main +// Package cellstyle demonstrates styling individual cells with fills, borders, and padding. +package cellstyle import ( - "context" - "log" - "github.com/avdoseferovic/paper" "github.com/avdoseferovic/paper/pkg/consts" "github.com/avdoseferovic/paper/pkg/core" @@ -19,24 +16,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/cellstyle.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/cellstyle.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the cellstyle example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithDebug(false). diff --git a/docs/assets/examples/cellstyle/main_test.go b/docs/assets/examples/cellstyle/paper_test.go similarity index 92% rename from docs/assets/examples/cellstyle/main_test.go rename to docs/assets/examples/cellstyle/paper_test.go index 1b56d0b8..ed69c3b1 100644 --- a/docs/assets/examples/cellstyle/main_test.go +++ b/docs/assets/examples/cellstyle/paper_test.go @@ -1,4 +1,4 @@ -package main +package cellstyle import ( "testing" diff --git a/docs/assets/examples/checkbox/cmd/main.go b/docs/assets/examples/checkbox/cmd/main.go new file mode 100644 index 00000000..85b12556 --- /dev/null +++ b/docs/assets/examples/checkbox/cmd/main.go @@ -0,0 +1,28 @@ +// Command checkbox writes the checkbox example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/checkbox/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/checkbox" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/checkbox.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/checkbox.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/checkbox/main.go b/docs/assets/examples/checkbox/paper.go similarity index 73% rename from docs/assets/examples/checkbox/main.go rename to docs/assets/examples/checkbox/paper.go index e726cea1..df8f51cf 100644 --- a/docs/assets/examples/checkbox/main.go +++ b/docs/assets/examples/checkbox/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates checkbox components with and without labels. -package main +// Package checkbox demonstrates checkbox components with and without labels. +package checkbox import ( - "context" - "log" - "github.com/avdoseferovic/paper" "github.com/avdoseferovic/paper/pkg/components/checkbox" "github.com/avdoseferovic/paper/pkg/components/text" @@ -14,24 +11,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/checkbox.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/checkbox.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the checkbox example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithDebug(true). @@ -40,7 +20,6 @@ func GetPaper() core.Paper { mrt := paper.New(cfg) m := decorator.NewMetrics(mrt) - // Default: unchecked and checked with label m.AddRow(20, checkbox.NewCol(4, "Option A"), checkbox.NewCol(4, "Option B", props.Checkbox{Checked: true}), @@ -48,7 +27,6 @@ func GetPaper() core.Paper { ) m.AddRows(text.NewRow(8, "Default: unchecked / checked / unchecked")) - // Custom size m.AddRow(20, checkbox.NewCol(4, "Option A", props.Checkbox{Size: 8}), checkbox.NewCol(4, "Option B", props.Checkbox{Size: 8, Checked: true}), @@ -56,7 +34,6 @@ func GetPaper() core.Paper { ) m.AddRows(text.NewRow(8, "Custom size: 8mm")) - // With top and left offset m.AddRow(20, checkbox.NewCol(4, "Option A", props.Checkbox{Top: 5, Left: 5}), checkbox.NewCol(4, "Option B", props.Checkbox{Top: 5, Left: 5, Checked: true}), @@ -64,7 +41,6 @@ func GetPaper() core.Paper { ) m.AddRows(text.NewRow(8, "With top=5 left=5 offset")) - // Auto row m.AddAutoRow( checkbox.NewCol(3, "Item 1"), checkbox.NewCol(3, "Item 2", props.Checkbox{Checked: true}), diff --git a/docs/assets/examples/checkbox/main_test.go b/docs/assets/examples/checkbox/paper_test.go similarity index 93% rename from docs/assets/examples/checkbox/main_test.go rename to docs/assets/examples/checkbox/paper_test.go index ac98c676..bbd857b6 100644 --- a/docs/assets/examples/checkbox/main_test.go +++ b/docs/assets/examples/checkbox/paper_test.go @@ -1,4 +1,4 @@ -package main +package checkbox import ( "testing" diff --git a/docs/assets/examples/compression/cmd/main.go b/docs/assets/examples/compression/cmd/main.go new file mode 100644 index 00000000..189450f3 --- /dev/null +++ b/docs/assets/examples/compression/cmd/main.go @@ -0,0 +1,32 @@ +// Command compression writes the compression example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/compression/cmd +package main + +import ( + "context" + "log" + + "github.com/avdoseferovic/paper/docs/assets/examples/compression" +) + +func main() { + m, err := compression.GetPaper("docs/assets/images/frontpage.png") + if err != nil { + log.Fatal(err.Error()) + } + + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/compression.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/compression.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/compression/main_test.go b/docs/assets/examples/compression/main_test.go deleted file mode 100644 index 20606726..00000000 --- a/docs/assets/examples/compression/main_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import ( - "os" - "path" - "strings" - "testing" - - "github.com/avdoseferovic/paper/internal/test" -) - -func TestGetPaper(t *testing.T) { - t.Parallel() - // Act - path := "docs/assets/images/frontpage.png" - sut := GetPaper(buildPath(path)) - - // Assert - test.New(t).Assert(sut.GetStructure()).Equals("examples/compression.json") -} - -func buildPath(file string) string { - dir, err := os.Getwd() - if err != nil { - return "" - } - - dir = strings.ReplaceAll(dir, "docs/assets/examples/compression", "") - return path.Join(dir, file) -} diff --git a/docs/assets/examples/compression/main.go b/docs/assets/examples/compression/paper.go similarity index 79% rename from docs/assets/examples/compression/main.go rename to docs/assets/examples/compression/paper.go index 0fd2e553..d37d618f 100644 --- a/docs/assets/examples/compression/main.go +++ b/docs/assets/examples/compression/paper.go @@ -1,10 +1,8 @@ -// Package main demonstrates turning content-stream compression on and off. -package main +// Package compression demonstrates turning content-stream compression on and off. +package compression import ( - "context" "fmt" - "log" "os" "github.com/avdoseferovic/paper/pkg/components/code" @@ -25,25 +23,11 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper("docs/assets/images/frontpage.png") - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/compression.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/compression.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - -func GetPaper(imagePath string) core.Paper { +// GetPaper builds the compression demo document. It returns an error when the +// image cannot be read so callers decide how to fail; the builder itself must +// never terminate the process, because it also runs inside the wasm playground +// where os.Exit would kill every registered export. +func GetPaper(imagePath string) (core.Paper, error) { cfg := config.NewBuilder(). WithCompression(true). Build() @@ -72,8 +56,7 @@ func GetPaper(imagePath string) core.Paper { bytes, err := os.ReadFile(imagePath) if err != nil { - fmt.Println("Got error while opening file:", err) - os.Exit(1) + return nil, fmt.Errorf("read image %s: %w", imagePath, err) } m.AddRows( row.New(20).Add( @@ -98,5 +81,5 @@ func GetPaper(imagePath string) core.Paper { ), ) - return m + return m, nil } diff --git a/docs/assets/examples/compression/paper_test.go b/docs/assets/examples/compression/paper_test.go new file mode 100644 index 00000000..3c362498 --- /dev/null +++ b/docs/assets/examples/compression/paper_test.go @@ -0,0 +1,50 @@ +package compression + +import ( + "os" + "path" + "strings" + "testing" + + "github.com/avdoseferovic/paper/internal/test" +) + +func TestGetPaper(t *testing.T) { + t.Parallel() + // Act + path := "docs/assets/images/frontpage.png" + sut, err := GetPaper(buildPath(path)) + // Assert + if err != nil { + t.Fatalf("GetPaper returned an error: %v", err) + } + test.New(t).Assert(sut.GetStructure()).Equals("examples/compression.json") +} + +// TestGetPaperMissingImage pins the contract that a missing image is reported as +// an error. The builder used to call os.Exit(1) here, which no recover() can +// catch: under wasm that tears down the instance and permanently disables every +// export on the page, and in a test run it kills the test binary outright. +func TestGetPaperMissingImage(t *testing.T) { + t.Parallel() + // Act + sut, err := GetPaper(buildPath("docs/assets/images/does-not-exist.png")) + + // Assert + if err == nil { + t.Fatal("expected an error for a missing image, got nil") + } + if sut != nil { + t.Errorf("expected a nil Paper alongside the error, got %T", sut) + } +} + +func buildPath(file string) string { + dir, err := os.Getwd() + if err != nil { + return "" + } + + dir = strings.ReplaceAll(dir, "docs/assets/examples/compression", "") + return path.Join(dir, file) +} diff --git a/docs/assets/examples/customdimensions/cmd/main.go b/docs/assets/examples/customdimensions/cmd/main.go new file mode 100644 index 00000000..ea22d9e7 --- /dev/null +++ b/docs/assets/examples/customdimensions/cmd/main.go @@ -0,0 +1,28 @@ +// Command customdimensions writes the customdimensions example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/customdimensions/cmd +package main + +import ( + "context" + "log" + + "github.com/avdoseferovic/paper/docs/assets/examples/customdimensions" +) + +func main() { + m := customdimensions.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/customdimensions.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/customdimensions.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/customdimensions/main.go b/docs/assets/examples/customdimensions/paper.go similarity index 63% rename from docs/assets/examples/customdimensions/main.go rename to docs/assets/examples/customdimensions/paper.go index 402d4920..8e9b065d 100644 --- a/docs/assets/examples/customdimensions/main.go +++ b/docs/assets/examples/customdimensions/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates a page sized in millimetres instead of a named page size. -package main +// Package customdimensions demonstrates a page sized in millimetres instead of a named page size. +package customdimensions import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/core" "github.com/avdoseferovic/paper" @@ -18,24 +15,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/customdimensions.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/customdimensions.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the customdimensions example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithDimensions(200, 200). diff --git a/docs/assets/examples/customdimensions/main_test.go b/docs/assets/examples/customdimensions/paper_test.go similarity index 90% rename from docs/assets/examples/customdimensions/main_test.go rename to docs/assets/examples/customdimensions/paper_test.go index e18c00dd..4c154839 100644 --- a/docs/assets/examples/customdimensions/main_test.go +++ b/docs/assets/examples/customdimensions/paper_test.go @@ -1,4 +1,4 @@ -package main +package customdimensions import ( "testing" diff --git a/docs/assets/examples/customfont/cmd/main.go b/docs/assets/examples/customfont/cmd/main.go new file mode 100644 index 00000000..d93ae4ea --- /dev/null +++ b/docs/assets/examples/customfont/cmd/main.go @@ -0,0 +1,28 @@ +// Command customfont writes the customfont example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/customfont/cmd +package main + +import ( + "context" + "log" + + "github.com/avdoseferovic/paper/docs/assets/examples/customfont" +) + +func main() { + m := customfont.GetPaper("docs/assets/fonts/arial-unicode-ms.ttf") + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/customfont.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/customfont.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/customfont/main.go b/docs/assets/examples/customfont/paper.go similarity index 95% rename from docs/assets/examples/customfont/main.go rename to docs/assets/examples/customfont/paper.go index d8b4b352..1e719d37 100644 --- a/docs/assets/examples/customfont/main.go +++ b/docs/assets/examples/customfont/paper.go @@ -1,8 +1,7 @@ -// Package main demonstrates registering a TrueType font and drawing UTF-8 text with it. -package main +// Package customfont demonstrates registering a TrueType font and drawing UTF-8 text with it. +package customfont import ( - "context" "log" "github.com/avdoseferovic/paper/pkg/consts" @@ -20,24 +19,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper("docs/assets/fonts/arial-unicode-ms.ttf") - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/customfont.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/customfont.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the customfont example document. func GetPaper(customFontFile string) core.Paper { customFont := "arial-unicode-ms" diff --git a/docs/assets/examples/customfont/main_test.go b/docs/assets/examples/customfont/paper_test.go similarity index 96% rename from docs/assets/examples/customfont/main_test.go rename to docs/assets/examples/customfont/paper_test.go index fa271db3..2f62bcac 100644 --- a/docs/assets/examples/customfont/main_test.go +++ b/docs/assets/examples/customfont/paper_test.go @@ -1,4 +1,4 @@ -package main +package customfont import ( "os" diff --git a/docs/assets/examples/custompage/cmd/main.go b/docs/assets/examples/custompage/cmd/main.go new file mode 100644 index 00000000..67bb2b60 --- /dev/null +++ b/docs/assets/examples/custompage/cmd/main.go @@ -0,0 +1,28 @@ +// Command custompage writes the custompage example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/custompage/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/custompage" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/custompage.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/custompage.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/custompage/main.go b/docs/assets/examples/custompage/paper.go similarity index 65% rename from docs/assets/examples/custompage/main.go rename to docs/assets/examples/custompage/paper.go index 5ba7c34d..fd195d8c 100644 --- a/docs/assets/examples/custompage/main.go +++ b/docs/assets/examples/custompage/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates building a document from a custom page definition. -package main +// Package custompage demonstrates building a document from a custom page definition. +package custompage import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/core" "github.com/avdoseferovic/paper" @@ -20,24 +17,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/custompage.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/custompage.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the custompage example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithPageSize(pagesize.A2). diff --git a/docs/assets/examples/custompage/main_test.go b/docs/assets/examples/custompage/paper_test.go similarity index 92% rename from docs/assets/examples/custompage/main_test.go rename to docs/assets/examples/custompage/paper_test.go index 380098a5..13e40b9d 100644 --- a/docs/assets/examples/custompage/main_test.go +++ b/docs/assets/examples/custompage/paper_test.go @@ -1,4 +1,4 @@ -package main +package custompage import ( "testing" diff --git a/docs/assets/examples/datamatrixgrid/cmd/main.go b/docs/assets/examples/datamatrixgrid/cmd/main.go new file mode 100644 index 00000000..3e6825a5 --- /dev/null +++ b/docs/assets/examples/datamatrixgrid/cmd/main.go @@ -0,0 +1,28 @@ +// Command datamatrixgrid writes the datamatrixgrid example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/datamatrixgrid/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/datamatrixgrid" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/datamatrixgrid.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/datamatrixgrid.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/datamatrixgrid/main.go b/docs/assets/examples/datamatrixgrid/paper.go similarity index 83% rename from docs/assets/examples/datamatrixgrid/main.go rename to docs/assets/examples/datamatrixgrid/paper.go index a6ddbd5f..ac94b1f4 100644 --- a/docs/assets/examples/datamatrixgrid/main.go +++ b/docs/assets/examples/datamatrixgrid/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates placing Data Matrix codes across a grid of columns. -package main +// Package datamatrixgrid demonstrates placing Data Matrix codes across a grid of columns. +package datamatrixgrid import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/core" "github.com/avdoseferovic/paper" @@ -16,24 +13,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/datamatrixgrid.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/datamatrixgrid.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the datamatrixgrid example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithDebug(true). diff --git a/docs/assets/examples/datamatrixgrid/main_test.go b/docs/assets/examples/datamatrixgrid/paper_test.go similarity index 91% rename from docs/assets/examples/datamatrixgrid/main_test.go rename to docs/assets/examples/datamatrixgrid/paper_test.go index e5e2b1d4..0b516268 100644 --- a/docs/assets/examples/datamatrixgrid/main_test.go +++ b/docs/assets/examples/datamatrixgrid/paper_test.go @@ -1,4 +1,4 @@ -package main +package datamatrixgrid import ( "testing" diff --git a/docs/assets/examples/disablepagebreak/cmd/main.go b/docs/assets/examples/disablepagebreak/cmd/main.go new file mode 100644 index 00000000..0c3d3431 --- /dev/null +++ b/docs/assets/examples/disablepagebreak/cmd/main.go @@ -0,0 +1,29 @@ +// Command disablepagebreak writes the disablepagebreak example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/disablepagebreak/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/disablepagebreak" +) + +func main() { + backgroundImage := "docs/assets/images/certificate.png" + m := example.GetPaper(backgroundImage) + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/disablepagebreak.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/disablepagebreak.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/disablepagebreak/main.go b/docs/assets/examples/disablepagebreak/paper.go similarity index 61% rename from docs/assets/examples/disablepagebreak/main.go rename to docs/assets/examples/disablepagebreak/paper.go index 5aeb1669..294c3cae 100644 --- a/docs/assets/examples/disablepagebreak/main.go +++ b/docs/assets/examples/disablepagebreak/paper.go @@ -1,8 +1,7 @@ -// Package main demonstrates keeping a row on one page instead of letting it split. -package main +// Package disablepagebreak demonstrates keeping a row on one page instead of letting it split. +package disablepagebreak import ( - "context" "log" "os" @@ -17,25 +16,7 @@ import ( "github.com/avdoseferovic/paper/pkg/core" ) -func main() { - backgroundImage := "docs/assets/images/certificate.png" - m := GetPaper(backgroundImage) - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/disablepagebreak.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/disablepagebreak.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the disablepagebreak example document. func GetPaper(image string) core.Paper { bytes, err := os.ReadFile(image) if err != nil { diff --git a/docs/assets/examples/disablepagebreak/main_test.go b/docs/assets/examples/disablepagebreak/paper_test.go similarity index 95% rename from docs/assets/examples/disablepagebreak/main_test.go rename to docs/assets/examples/disablepagebreak/paper_test.go index 529cdfc9..4d2b2c6c 100644 --- a/docs/assets/examples/disablepagebreak/main_test.go +++ b/docs/assets/examples/disablepagebreak/paper_test.go @@ -1,4 +1,4 @@ -package main +package disablepagebreak import ( "os" diff --git a/docs/assets/examples/footer/cmd/main.go b/docs/assets/examples/footer/cmd/main.go new file mode 100644 index 00000000..3d3f69e2 --- /dev/null +++ b/docs/assets/examples/footer/cmd/main.go @@ -0,0 +1,28 @@ +// Command footer writes the footer example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/footer/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/footer" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/footer.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/footer.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/footer/main.go b/docs/assets/examples/footer/paper.go similarity index 65% rename from docs/assets/examples/footer/main.go rename to docs/assets/examples/footer/paper.go index df55e84e..6bce1a46 100644 --- a/docs/assets/examples/footer/main.go +++ b/docs/assets/examples/footer/paper.go @@ -1,8 +1,7 @@ -// Package main demonstrates a footer that repeats at the bottom of every page. -package main +// Package footer demonstrates a footer that repeats at the bottom of every page. +package footer import ( - "context" "log" "github.com/avdoseferovic/paper/pkg/consts" @@ -19,24 +18,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/footer.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/footer.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the footer example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithDebug(true). diff --git a/docs/assets/examples/footer/main_test.go b/docs/assets/examples/footer/paper_test.go similarity index 93% rename from docs/assets/examples/footer/main_test.go rename to docs/assets/examples/footer/paper_test.go index 94043861..c660524b 100644 --- a/docs/assets/examples/footer/main_test.go +++ b/docs/assets/examples/footer/paper_test.go @@ -1,4 +1,4 @@ -package main +package footer import ( "testing" diff --git a/docs/assets/examples/header/cmd/main.go b/docs/assets/examples/header/cmd/main.go new file mode 100644 index 00000000..25297315 --- /dev/null +++ b/docs/assets/examples/header/cmd/main.go @@ -0,0 +1,28 @@ +// Command header writes the header example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/header/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/header" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/header.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/header.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/header/main.go b/docs/assets/examples/header/paper.go similarity index 65% rename from docs/assets/examples/header/main.go rename to docs/assets/examples/header/paper.go index e21e8340..bbda4136 100644 --- a/docs/assets/examples/header/main.go +++ b/docs/assets/examples/header/paper.go @@ -1,8 +1,7 @@ -// Package main demonstrates a header that repeats at the top of every page. -package main +// Package header demonstrates a header that repeats at the top of every page. +package header import ( - "context" "log" "github.com/avdoseferovic/paper/pkg/consts" @@ -19,24 +18,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/header.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/header.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the header example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithDebug(true). diff --git a/docs/assets/examples/header/main_test.go b/docs/assets/examples/header/paper_test.go similarity index 93% rename from docs/assets/examples/header/main_test.go rename to docs/assets/examples/header/paper_test.go index 793729fc..d2239248 100644 --- a/docs/assets/examples/header/main_test.go +++ b/docs/assets/examples/header/paper_test.go @@ -1,4 +1,4 @@ -package main +package header import ( "testing" diff --git a/docs/assets/examples/imagegrid/cmd/main.go b/docs/assets/examples/imagegrid/cmd/main.go new file mode 100644 index 00000000..20df94d4 --- /dev/null +++ b/docs/assets/examples/imagegrid/cmd/main.go @@ -0,0 +1,28 @@ +// Command imagegrid writes the imagegrid example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/imagegrid/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/imagegrid" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/imagegrid.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/imagegrid.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/imagegrid/main.go b/docs/assets/examples/imagegrid/paper.go similarity index 88% rename from docs/assets/examples/imagegrid/main.go rename to docs/assets/examples/imagegrid/paper.go index b3eaf143..c57a57e1 100644 --- a/docs/assets/examples/imagegrid/main.go +++ b/docs/assets/examples/imagegrid/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates placing images across a grid of columns. -package main +// Package imagegrid demonstrates placing images across a grid of columns. +package imagegrid import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/core" "github.com/avdoseferovic/paper" @@ -16,24 +13,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/imagegrid.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/imagegrid.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the imagegrid example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithDebug(true). diff --git a/docs/assets/examples/imagegrid/main_test.go b/docs/assets/examples/imagegrid/paper_test.go similarity index 92% rename from docs/assets/examples/imagegrid/main_test.go rename to docs/assets/examples/imagegrid/paper_test.go index 8bda45ec..8000a600 100644 --- a/docs/assets/examples/imagegrid/main_test.go +++ b/docs/assets/examples/imagegrid/paper_test.go @@ -1,4 +1,4 @@ -package main +package imagegrid import ( "testing" diff --git a/docs/assets/examples/line/cmd/main.go b/docs/assets/examples/line/cmd/main.go new file mode 100644 index 00000000..4f6d0da8 --- /dev/null +++ b/docs/assets/examples/line/cmd/main.go @@ -0,0 +1,28 @@ +// Command line writes the line example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/line/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/line" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/linegrid.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/linegrid.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/line/main.go b/docs/assets/examples/line/paper.go similarity index 83% rename from docs/assets/examples/line/main.go rename to docs/assets/examples/line/paper.go index 8e57faad..7714ab9c 100644 --- a/docs/assets/examples/line/main.go +++ b/docs/assets/examples/line/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates horizontal rules, including dashed and coloured styles. -package main +// Package line demonstrates horizontal rules, including dashed and coloured styles. +package line import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/consts" "github.com/avdoseferovic/paper/pkg/core" @@ -17,24 +14,7 @@ import ( "github.com/avdoseferovic/paper/pkg/config" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/linegrid.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/linegrid.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the line example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithDebug(true). diff --git a/docs/assets/examples/line/main_test.go b/docs/assets/examples/line/paper_test.go similarity index 94% rename from docs/assets/examples/line/main_test.go rename to docs/assets/examples/line/paper_test.go index 064e2b69..6df8f9a8 100644 --- a/docs/assets/examples/line/main_test.go +++ b/docs/assets/examples/line/paper_test.go @@ -1,4 +1,4 @@ -package main +package line import ( "testing" diff --git a/docs/assets/examples/list/cmd/main.go b/docs/assets/examples/list/cmd/main.go new file mode 100644 index 00000000..a57dd29c --- /dev/null +++ b/docs/assets/examples/list/cmd/main.go @@ -0,0 +1,28 @@ +// Command list writes the list example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/list/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/list" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/list.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/list.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/list/main.go b/docs/assets/examples/list/paper.go similarity index 77% rename from docs/assets/examples/list/main.go rename to docs/assets/examples/list/paper.go index 60a0292e..7e29a7cc 100644 --- a/docs/assets/examples/list/main.go +++ b/docs/assets/examples/list/paper.go @@ -1,8 +1,7 @@ -// Package main demonstrates rendering a slice of values as a table with a header row. -package main +// Package list demonstrates rendering a slice of values as a table with a header row. +package list import ( - "context" "fmt" "log" @@ -24,24 +23,7 @@ var background = &props.Color{ Blue: 200, } -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/list.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/list.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the list example document. func GetPaper() core.Paper { mrt := paper.New() m := decorator.NewMetrics(mrt) @@ -56,11 +38,14 @@ func GetPaper() core.Paper { return m } +// Object is one key/value row rendered by the list example. It implements the +// list component's interfaces so a slice of Objects can be built into a table. type Object struct { Key string Value string } +// GetHeader is part of the list example. func (o Object) GetHeader() core.Row { return row.New(10).Add( text.NewCol(4, "Key", props.Text{Style: fontstyle.Bold}), @@ -68,6 +53,7 @@ func (o Object) GetHeader() core.Row { ) } +// GetContent is part of the list example. func (o Object) GetContent(i int) core.Row { r := row.New(5).Add( text.NewCol(4, o.Key), diff --git a/docs/assets/examples/list/main_test.go b/docs/assets/examples/list/paper_test.go similarity index 94% rename from docs/assets/examples/list/main_test.go rename to docs/assets/examples/list/paper_test.go index 9d2f8f32..f8c460a9 100644 --- a/docs/assets/examples/list/main_test.go +++ b/docs/assets/examples/list/paper_test.go @@ -1,4 +1,4 @@ -package main +package list import ( "testing" diff --git a/docs/assets/examples/lowmemory/cmd/main.go b/docs/assets/examples/lowmemory/cmd/main.go new file mode 100644 index 00000000..2ff8d1cf --- /dev/null +++ b/docs/assets/examples/lowmemory/cmd/main.go @@ -0,0 +1,28 @@ +// Command lowmemory writes the lowmemory example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/lowmemory/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/lowmemory" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/lowmemory.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/lowmemory.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/lowmemory/main.go b/docs/assets/examples/lowmemory/paper.go similarity index 56% rename from docs/assets/examples/lowmemory/main.go rename to docs/assets/examples/lowmemory/paper.go index aafed98b..2aa50f08 100644 --- a/docs/assets/examples/lowmemory/main.go +++ b/docs/assets/examples/lowmemory/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates low-memory mode, which writes pages out as they are built. -package main +// Package lowmemory demonstrates low-memory mode, which writes pages out as they are built. +package lowmemory import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/core" "github.com/avdoseferovic/paper" @@ -16,24 +13,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/lowmemory.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/lowmemory.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the lowmemory example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithSequentialLowMemoryMode(7). diff --git a/docs/assets/examples/lowmemory/main_test.go b/docs/assets/examples/lowmemory/paper_test.go similarity index 92% rename from docs/assets/examples/lowmemory/main_test.go rename to docs/assets/examples/lowmemory/paper_test.go index 2fe47f35..104d38c3 100644 --- a/docs/assets/examples/lowmemory/main_test.go +++ b/docs/assets/examples/lowmemory/paper_test.go @@ -1,4 +1,4 @@ -package main +package lowmemory import ( "testing" diff --git a/docs/assets/examples/margins/cmd/main.go b/docs/assets/examples/margins/cmd/main.go new file mode 100644 index 00000000..b078d284 --- /dev/null +++ b/docs/assets/examples/margins/cmd/main.go @@ -0,0 +1,28 @@ +// Command margins writes the margins example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/margins/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/margins" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/margins.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/margins.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/margins/main.go b/docs/assets/examples/margins/paper.go similarity index 71% rename from docs/assets/examples/margins/main.go rename to docs/assets/examples/margins/paper.go index be209473..e6e28701 100644 --- a/docs/assets/examples/margins/main.go +++ b/docs/assets/examples/margins/paper.go @@ -1,8 +1,7 @@ -// Package main demonstrates setting the page margins. -package main +// Package margins demonstrates setting the page margins. +package margins import ( - "context" "log" "github.com/avdoseferovic/paper/pkg/components/row" @@ -17,24 +16,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/margins.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/margins.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the margins example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithTopMargin(20). diff --git a/docs/assets/examples/margins/main_test.go b/docs/assets/examples/margins/paper_test.go similarity index 93% rename from docs/assets/examples/margins/main_test.go rename to docs/assets/examples/margins/paper_test.go index 7591cde9..7da68e36 100644 --- a/docs/assets/examples/margins/main_test.go +++ b/docs/assets/examples/margins/paper_test.go @@ -1,4 +1,4 @@ -package main +package margins import ( "testing" diff --git a/docs/assets/examples/maxgridsum/cmd/main.go b/docs/assets/examples/maxgridsum/cmd/main.go new file mode 100644 index 00000000..86ab3839 --- /dev/null +++ b/docs/assets/examples/maxgridsum/cmd/main.go @@ -0,0 +1,28 @@ +// Command maxgridsum writes the maxgridsum example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/maxgridsum/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/maxgridsum" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/maxgridsum.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/maxgridsum.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/maxgridsum/main.go b/docs/assets/examples/maxgridsum/paper.go similarity index 69% rename from docs/assets/examples/maxgridsum/main.go rename to docs/assets/examples/maxgridsum/paper.go index 51baee16..bc86cc15 100644 --- a/docs/assets/examples/maxgridsum/main.go +++ b/docs/assets/examples/maxgridsum/paper.go @@ -1,10 +1,8 @@ -// Package main demonstrates changing how many grid units a row spans. -package main +// Package maxgridsum demonstrates changing how many grid units a row spans. +package maxgridsum import ( - "context" "fmt" - "log" "github.com/avdoseferovic/paper" "github.com/avdoseferovic/paper/pkg/decorator" @@ -18,24 +16,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/maxgridsum.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/maxgridsum.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the maxgridsum example document. func GetPaper() core.Paper { gridSum := 14 cfg := config.NewBuilder(). diff --git a/docs/assets/examples/maxgridsum/main_test.go b/docs/assets/examples/maxgridsum/paper_test.go similarity index 92% rename from docs/assets/examples/maxgridsum/main_test.go rename to docs/assets/examples/maxgridsum/paper_test.go index 2c2f8ca5..38a93cd2 100644 --- a/docs/assets/examples/maxgridsum/main_test.go +++ b/docs/assets/examples/maxgridsum/paper_test.go @@ -1,4 +1,4 @@ -package main +package maxgridsum import ( "testing" diff --git a/docs/assets/examples/mergepdf/main.go b/docs/assets/examples/mergepdf/cmd/main.go similarity index 51% rename from docs/assets/examples/mergepdf/main.go rename to docs/assets/examples/mergepdf/cmd/main.go index 0fd8768b..c6645751 100644 --- a/docs/assets/examples/mergepdf/main.go +++ b/docs/assets/examples/mergepdf/cmd/main.go @@ -1,4 +1,5 @@ -// Package main demonstrates merging a generated document with another PDF. +// Command mergepdf writes the mergepdf example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/mergepdf/cmd package main import ( @@ -6,17 +7,11 @@ import ( "log" "os" - "github.com/avdoseferovic/paper/pkg/core" - - "github.com/avdoseferovic/paper" - "github.com/avdoseferovic/paper/pkg/decorator" - - "github.com/avdoseferovic/paper/pkg/components/text" - "github.com/avdoseferovic/paper/pkg/config" + example "github.com/avdoseferovic/paper/docs/assets/examples/mergepdf" ) func main() { - m := GetPaper() + m := example.GetPaper() document, err := m.Generate(context.Background()) if err != nil { log.Fatal(err.Error()) @@ -42,18 +37,3 @@ func main() { log.Fatal(err.Error()) } } - -func GetPaper() core.Paper { - cfg := config.NewBuilder(). - WithPageNumber(). - Build() - - mrt := paper.New(cfg) - m := decorator.NewMetrics(mrt) - - for range 50 { - m.AddRows(text.NewRow(20, "content")) - } - - return m -} diff --git a/docs/assets/examples/mergepdf/paper.go b/docs/assets/examples/mergepdf/paper.go new file mode 100644 index 00000000..5b6cd646 --- /dev/null +++ b/docs/assets/examples/mergepdf/paper.go @@ -0,0 +1,28 @@ +// Package mergepdf demonstrates merging a generated document with another PDF. +package mergepdf + +import ( + "github.com/avdoseferovic/paper/pkg/core" + + "github.com/avdoseferovic/paper" + "github.com/avdoseferovic/paper/pkg/decorator" + + "github.com/avdoseferovic/paper/pkg/components/text" + "github.com/avdoseferovic/paper/pkg/config" +) + +// GetPaper builds the mergepdf example document. +func GetPaper() core.Paper { + cfg := config.NewBuilder(). + WithPageNumber(). + Build() + + mrt := paper.New(cfg) + m := decorator.NewMetrics(mrt) + + for range 50 { + m.AddRows(text.NewRow(20, "content")) + } + + return m +} diff --git a/docs/assets/examples/mergepdf/main_test.go b/docs/assets/examples/mergepdf/paper_test.go similarity index 93% rename from docs/assets/examples/mergepdf/main_test.go rename to docs/assets/examples/mergepdf/paper_test.go index 5cae0d7f..5f46e821 100644 --- a/docs/assets/examples/mergepdf/main_test.go +++ b/docs/assets/examples/mergepdf/paper_test.go @@ -1,4 +1,4 @@ -package main +package mergepdf import ( "testing" diff --git a/docs/assets/examples/metadatas/cmd/main.go b/docs/assets/examples/metadatas/cmd/main.go new file mode 100644 index 00000000..79bc7d4b --- /dev/null +++ b/docs/assets/examples/metadatas/cmd/main.go @@ -0,0 +1,28 @@ +// Command metadatas writes the metadatas example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/metadatas/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/metadatas" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/metadatas.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/metadatas.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/metadatas/main.go b/docs/assets/examples/metadatas/paper.go similarity index 57% rename from docs/assets/examples/metadatas/main.go rename to docs/assets/examples/metadatas/paper.go index 3544b733..3d1e9900 100644 --- a/docs/assets/examples/metadatas/main.go +++ b/docs/assets/examples/metadatas/paper.go @@ -1,9 +1,7 @@ -// Package main demonstrates setting document metadata such as title, author, and subject. -package main +// Package metadatas demonstrates setting document metadata such as title, author, and subject. +package metadatas import ( - "context" - "log" "time" "github.com/avdoseferovic/paper/pkg/core" @@ -15,24 +13,7 @@ import ( "github.com/avdoseferovic/paper/pkg/config" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/metadatas.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/metadatas.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the metadatas example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithAuthor("author", false). diff --git a/docs/assets/examples/metadatas/main_test.go b/docs/assets/examples/metadatas/paper_test.go similarity index 92% rename from docs/assets/examples/metadatas/main_test.go rename to docs/assets/examples/metadatas/paper_test.go index 72a223cb..7cffcf17 100644 --- a/docs/assets/examples/metadatas/main_test.go +++ b/docs/assets/examples/metadatas/paper_test.go @@ -1,4 +1,4 @@ -package main +package metadatas import ( "testing" diff --git a/docs/assets/examples/orientation/cmd/main.go b/docs/assets/examples/orientation/cmd/main.go new file mode 100644 index 00000000..650554c3 --- /dev/null +++ b/docs/assets/examples/orientation/cmd/main.go @@ -0,0 +1,28 @@ +// Command orientation writes the orientation example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/orientation/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/orientation" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/orientation.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/orientation.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/orientation/main.go b/docs/assets/examples/orientation/paper.go similarity index 55% rename from docs/assets/examples/orientation/main.go rename to docs/assets/examples/orientation/paper.go index d0ae6b3a..b09e6916 100644 --- a/docs/assets/examples/orientation/main.go +++ b/docs/assets/examples/orientation/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates landscape orientation. -package main +// Package orientation demonstrates landscape orientation. +package orientation import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/consts" "github.com/avdoseferovic/paper/pkg/core" @@ -15,24 +12,7 @@ import ( "github.com/avdoseferovic/paper/pkg/config" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/orientation.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/orientation.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the orientation example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithOrientation(consts.OrientationHorizontal). diff --git a/docs/assets/examples/orientation/main_test.go b/docs/assets/examples/orientation/paper_test.go similarity index 92% rename from docs/assets/examples/orientation/main_test.go rename to docs/assets/examples/orientation/paper_test.go index bae6c47b..aaa71411 100644 --- a/docs/assets/examples/orientation/main_test.go +++ b/docs/assets/examples/orientation/paper_test.go @@ -1,4 +1,4 @@ -package main +package orientation import ( "testing" diff --git a/docs/assets/examples/pagenumber/cmd/main.go b/docs/assets/examples/pagenumber/cmd/main.go new file mode 100644 index 00000000..161fa0fa --- /dev/null +++ b/docs/assets/examples/pagenumber/cmd/main.go @@ -0,0 +1,28 @@ +// Command pagenumber writes the pagenumber example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/pagenumber/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/pagenumber" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/pagenumber.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/pagenumber.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/pagenumber/main.go b/docs/assets/examples/pagenumber/paper.go similarity index 66% rename from docs/assets/examples/pagenumber/main.go rename to docs/assets/examples/pagenumber/paper.go index 3e089c8f..9bd82194 100644 --- a/docs/assets/examples/pagenumber/main.go +++ b/docs/assets/examples/pagenumber/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates page numbering in a footer. -package main +// Package pagenumber demonstrates page numbering in a footer. +package pagenumber import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/consts" "github.com/avdoseferovic/paper/pkg/consts/fontstyle" @@ -18,24 +15,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/pagenumber.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/pagenumber.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the pagenumber example document. func GetPaper() core.Paper { pageNumber := props.PageNumber{ Pattern: "Page {current} of {total}", diff --git a/docs/assets/examples/pagenumber/main_test.go b/docs/assets/examples/pagenumber/paper_test.go similarity index 92% rename from docs/assets/examples/pagenumber/main_test.go rename to docs/assets/examples/pagenumber/paper_test.go index e29462f3..1f9d6428 100644 --- a/docs/assets/examples/pagenumber/main_test.go +++ b/docs/assets/examples/pagenumber/paper_test.go @@ -1,4 +1,4 @@ -package main +package pagenumber import ( "testing" diff --git a/docs/assets/examples/parallelism/cmd/main.go b/docs/assets/examples/parallelism/cmd/main.go new file mode 100644 index 00000000..85a1dcda --- /dev/null +++ b/docs/assets/examples/parallelism/cmd/main.go @@ -0,0 +1,28 @@ +// Command parallelism writes the parallelism example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/parallelism/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/parallelism" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/parallelism.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/parallelism.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/parallelism/main.go b/docs/assets/examples/parallelism/paper.go similarity index 57% rename from docs/assets/examples/parallelism/main.go rename to docs/assets/examples/parallelism/paper.go index f2539479..fd798fda 100644 --- a/docs/assets/examples/parallelism/main.go +++ b/docs/assets/examples/parallelism/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates generating page chunks concurrently. -package main +// Package parallelism demonstrates generating page chunks concurrently. +package parallelism import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/core" "github.com/avdoseferovic/paper" @@ -16,24 +13,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/parallelism.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/parallelism.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the parallelism example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithParallelPagesMode(7). diff --git a/docs/assets/examples/parallelism/main_test.go b/docs/assets/examples/parallelism/paper_test.go similarity index 92% rename from docs/assets/examples/parallelism/main_test.go rename to docs/assets/examples/parallelism/paper_test.go index ab0a74ce..ba1b301d 100644 --- a/docs/assets/examples/parallelism/main_test.go +++ b/docs/assets/examples/parallelism/paper_test.go @@ -1,4 +1,4 @@ -package main +package parallelism import ( "testing" diff --git a/docs/assets/examples/protection/cmd/main.go b/docs/assets/examples/protection/cmd/main.go new file mode 100644 index 00000000..ccb40ae0 --- /dev/null +++ b/docs/assets/examples/protection/cmd/main.go @@ -0,0 +1,28 @@ +// Command protection writes the protection example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/protection/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/protection" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/protection.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/protection.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/protection/main.go b/docs/assets/examples/protection/paper.go similarity index 54% rename from docs/assets/examples/protection/main.go rename to docs/assets/examples/protection/paper.go index 19295561..c8527137 100644 --- a/docs/assets/examples/protection/main.go +++ b/docs/assets/examples/protection/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates password protection and permission flags. -package main +// Package protection demonstrates password protection and permission flags. +package protection import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/core" "github.com/avdoseferovic/paper" @@ -16,24 +13,7 @@ import ( "github.com/avdoseferovic/paper/pkg/config" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/protection.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/protection.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the protection example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithProtection(protection.None, "user", "owner"). diff --git a/docs/assets/examples/protection/main_test.go b/docs/assets/examples/protection/paper_test.go similarity index 92% rename from docs/assets/examples/protection/main_test.go rename to docs/assets/examples/protection/paper_test.go index 7421d780..e95c1d8a 100644 --- a/docs/assets/examples/protection/main_test.go +++ b/docs/assets/examples/protection/paper_test.go @@ -1,4 +1,4 @@ -package main +package protection import ( "testing" diff --git a/docs/assets/examples/qrgrid/cmd/main.go b/docs/assets/examples/qrgrid/cmd/main.go new file mode 100644 index 00000000..e28e2d7a --- /dev/null +++ b/docs/assets/examples/qrgrid/cmd/main.go @@ -0,0 +1,28 @@ +// Command qrgrid writes the qrgrid example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/qrgrid/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/qrgrid" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/qrgrid.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/qrgrid.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/qrgrid/main.go b/docs/assets/examples/qrgrid/paper.go similarity index 83% rename from docs/assets/examples/qrgrid/main.go rename to docs/assets/examples/qrgrid/paper.go index 1a3340b4..72889ff1 100644 --- a/docs/assets/examples/qrgrid/main.go +++ b/docs/assets/examples/qrgrid/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates placing QR codes across a grid of columns. -package main +// Package qrgrid demonstrates placing QR codes across a grid of columns. +package qrgrid import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/core" "github.com/avdoseferovic/paper" @@ -16,24 +13,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/qrgrid.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/qrgrid.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the qrgrid example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithDebug(true). diff --git a/docs/assets/examples/qrgrid/main_test.go b/docs/assets/examples/qrgrid/paper_test.go similarity index 93% rename from docs/assets/examples/qrgrid/main_test.go rename to docs/assets/examples/qrgrid/paper_test.go index 81bd05b8..01a9b342 100644 --- a/docs/assets/examples/qrgrid/main_test.go +++ b/docs/assets/examples/qrgrid/paper_test.go @@ -1,4 +1,4 @@ -package main +package qrgrid import ( "testing" diff --git a/docs/assets/examples/signaturegrid/cmd/main.go b/docs/assets/examples/signaturegrid/cmd/main.go new file mode 100644 index 00000000..729ec4fe --- /dev/null +++ b/docs/assets/examples/signaturegrid/cmd/main.go @@ -0,0 +1,28 @@ +// Command signaturegrid writes the signaturegrid example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/signaturegrid/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/signaturegrid" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/signaturegrid.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/signaturegrid.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/signaturegrid/main.go b/docs/assets/examples/signaturegrid/paper.go similarity index 77% rename from docs/assets/examples/signaturegrid/main.go rename to docs/assets/examples/signaturegrid/paper.go index b9fc11bb..80aa4e8a 100644 --- a/docs/assets/examples/signaturegrid/main.go +++ b/docs/assets/examples/signaturegrid/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates signature lines across a grid of columns. -package main +// Package signaturegrid demonstrates signature lines across a grid of columns. +package signaturegrid import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/consts" "github.com/avdoseferovic/paper/pkg/core" @@ -18,24 +15,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/signaturegrid.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/signaturegrid.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the signaturegrid example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithDebug(true). diff --git a/docs/assets/examples/signaturegrid/main_test.go b/docs/assets/examples/signaturegrid/paper_test.go similarity index 91% rename from docs/assets/examples/signaturegrid/main_test.go rename to docs/assets/examples/signaturegrid/paper_test.go index dff524a5..600fd372 100644 --- a/docs/assets/examples/signaturegrid/main_test.go +++ b/docs/assets/examples/signaturegrid/paper_test.go @@ -1,4 +1,4 @@ -package main +package signaturegrid import ( "testing" diff --git a/docs/assets/examples/simplest/cmd/main.go b/docs/assets/examples/simplest/cmd/main.go new file mode 100644 index 00000000..dccf17bc --- /dev/null +++ b/docs/assets/examples/simplest/cmd/main.go @@ -0,0 +1,23 @@ +// Command simplest writes the simplest example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/simplest/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/simplest" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err) + } + + err = document.Save("docs/assets/pdf/simplest.pdf") + if err != nil { + log.Fatal(err) + } +} diff --git a/docs/assets/examples/simplest/main.go b/docs/assets/examples/simplest/paper.go similarity index 74% rename from docs/assets/examples/simplest/main.go rename to docs/assets/examples/simplest/paper.go index b37461a1..f3355aa6 100644 --- a/docs/assets/examples/simplest/main.go +++ b/docs/assets/examples/simplest/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates the smallest document: one row with one line of text. -package main +// Package simplest demonstrates the smallest document: one row with one line of text. +package simplest import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/components/checkbox" "github.com/avdoseferovic/paper/pkg/core" @@ -20,19 +17,7 @@ import ( "github.com/avdoseferovic/paper/pkg/components/text" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err) - } - - err = document.Save("docs/assets/pdf/simplest.pdf") - if err != nil { - log.Fatal(err) - } -} - +// GetPaper builds the simplest example document. func GetPaper() core.Paper { m := paper.New() diff --git a/docs/assets/examples/simplest/main_test.go b/docs/assets/examples/simplest/paper_test.go similarity index 93% rename from docs/assets/examples/simplest/main_test.go rename to docs/assets/examples/simplest/paper_test.go index ff333621..4a340e59 100644 --- a/docs/assets/examples/simplest/main_test.go +++ b/docs/assets/examples/simplest/paper_test.go @@ -1,4 +1,4 @@ -package main +package simplest import ( "testing" diff --git a/docs/assets/examples/textgrid/cmd/main.go b/docs/assets/examples/textgrid/cmd/main.go new file mode 100644 index 00000000..c361dd73 --- /dev/null +++ b/docs/assets/examples/textgrid/cmd/main.go @@ -0,0 +1,28 @@ +// Command textgrid writes the textgrid example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/textgrid/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/textgrid" +) + +func main() { + m := example.GetPaper() + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/textgrid.pdf") + if err != nil { + log.Fatal(err.Error()) + } + + err = document.GetReport().Save("docs/assets/text/textgrid.txt") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/textgrid/main.go b/docs/assets/examples/textgrid/paper.go similarity index 89% rename from docs/assets/examples/textgrid/main.go rename to docs/assets/examples/textgrid/paper.go index 14e86e9f..2a4d062b 100644 --- a/docs/assets/examples/textgrid/main.go +++ b/docs/assets/examples/textgrid/paper.go @@ -1,10 +1,7 @@ -// Package main demonstrates text components across a grid of columns. -package main +// Package textgrid demonstrates text components across a grid of columns. +package textgrid import ( - "context" - "log" - "github.com/avdoseferovic/paper/pkg/consts" "github.com/avdoseferovic/paper/pkg/consts/fontstyle" @@ -19,24 +16,7 @@ import ( "github.com/avdoseferovic/paper/pkg/props" ) -func main() { - m := GetPaper() - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/textgrid.pdf") - if err != nil { - log.Fatal(err.Error()) - } - - err = document.GetReport().Save("docs/assets/text/textgrid.txt") - if err != nil { - log.Fatal(err.Error()) - } -} - +// GetPaper builds the textgrid example document. func GetPaper() core.Paper { cfg := config.NewBuilder(). WithDebug(true). diff --git a/docs/assets/examples/textgrid/main_test.go b/docs/assets/examples/textgrid/paper_test.go similarity index 93% rename from docs/assets/examples/textgrid/main_test.go rename to docs/assets/examples/textgrid/paper_test.go index b7fa46e1..7288e249 100644 --- a/docs/assets/examples/textgrid/main_test.go +++ b/docs/assets/examples/textgrid/paper_test.go @@ -1,4 +1,4 @@ -package main +package textgrid import ( "testing" diff --git a/docs/assets/examples/watermark/cmd/main.go b/docs/assets/examples/watermark/cmd/main.go new file mode 100644 index 00000000..4ccef16a --- /dev/null +++ b/docs/assets/examples/watermark/cmd/main.go @@ -0,0 +1,24 @@ +// Command watermark writes the watermark example's PDF into docs/assets/pdf. +// Run it from the repository root: go run ./docs/assets/examples/watermark/cmd +package main + +import ( + "context" + "log" + + example "github.com/avdoseferovic/paper/docs/assets/examples/watermark" +) + +func main() { + m := example.GetPaper() + + document, err := m.Generate(context.Background()) + if err != nil { + log.Fatal(err.Error()) + } + + err = document.Save("docs/assets/pdf/watermark.pdf") + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/docs/assets/examples/watermark/main.go b/docs/assets/examples/watermark/paper.go similarity index 62% rename from docs/assets/examples/watermark/main.go rename to docs/assets/examples/watermark/paper.go index 4ce9e29f..99c9d2e7 100644 --- a/docs/assets/examples/watermark/main.go +++ b/docs/assets/examples/watermark/paper.go @@ -1,19 +1,18 @@ -// Package main demonstrates the per-page text watermark: translucent +// Package watermark demonstrates the per-page text watermark: translucent // diagonal text drawn under the content of every page. -package main +package watermark import ( - "context" - "log" - "github.com/avdoseferovic/paper" "github.com/avdoseferovic/paper/pkg/components/col" "github.com/avdoseferovic/paper/pkg/components/text" "github.com/avdoseferovic/paper/pkg/config" + "github.com/avdoseferovic/paper/pkg/core" "github.com/avdoseferovic/paper/pkg/props" ) -func main() { +// GetPaper builds the watermark example document. +func GetPaper() core.Paper { cfg := config.NewBuilder(). WithWatermark("DRAFT"). Build() @@ -23,13 +22,5 @@ func main() { m.AddRow(250, col.New(12).Add(text.New("Body content flows over the watermark.", props.Text{Top: 5}))) } - document, err := m.Generate(context.Background()) - if err != nil { - log.Fatal(err.Error()) - } - - err = document.Save("docs/assets/pdf/watermark.pdf") - if err != nil { - log.Fatal(err.Error()) - } + return m } diff --git a/docs/assets/examples/watermark/paper_test.go b/docs/assets/examples/watermark/paper_test.go new file mode 100644 index 00000000..f58c1e49 --- /dev/null +++ b/docs/assets/examples/watermark/paper_test.go @@ -0,0 +1,16 @@ +package watermark + +import ( + "testing" + + "github.com/avdoseferovic/paper/internal/test" +) + +func TestGetPaper(t *testing.T) { + t.Parallel() + // Act + sut := GetPaper() + + // Assert + test.New(t).Assert(sut.GetStructure()).Equals("examples/watermark.json") +} diff --git a/docs/assets/js/paper-fs.js b/docs/assets/js/paper-fs.js new file mode 100644 index 00000000..c3f94a11 --- /dev/null +++ b/docs/assets/js/paper-fs.js @@ -0,0 +1,261 @@ +// paper-fs.js — a read-only, in-memory filesystem for Go's js/wasm runtime. +// +// Go's `os.ReadFile` in a browser goes through `globalThis.fs`, which +// `wasm_exec.js` otherwise fills with stubs that answer ENOSYS to everything. +// That guard is `if (!globalThis.fs)`, so defining our own object *before* +// `wasm_exec.js` loads replaces the stub wholesale and lets the Paper library +// read images with no change to the library or to the documented example code. +// +// Load order matters: +// +// +// +// +// Only `fs` is defined here. `wasm_exec.js` installs `globalThis.process` and +// `globalThis.path` behind their own separate guards and those stubs are fine — +// `syscall.Open` calls `path.resolve()`, which the stub implements, and nothing +// on the read path calls `process.cwd()`. +// +// Files are registered by the exact path Go will ask for: +// +// paperFS.set("docs/assets/images/biplane.jpg", bytes); +// +// Paths are treated as opaque keys, not as a hierarchy — `syscall.Open` does not +// prepend the working directory, so whatever string the Go code passes to +// `os.ReadFile` is what arrives here. +(function () { + "use strict"; + + // Regular-file mode bits (S_IFREG). Without this, Go reads the entry as a + // directory and follows a readdir path that has no meaning here. + var S_IFREG = 0o100000; + + // Lowest fd we hand out. 0/1/2 stay reserved for stdin/stdout/stderr. + var FIRST_FD = 3; + + var files = new Map(); // path -> Uint8Array + var open = new Map(); // fd -> { path, bytes, cursor } + var nextFd = FIRST_FD; + + // Line buffers for fd 1 and 2, so Go's log output arrives at the console in + // whole lines rather than one call per write. + var outputBuf = { 1: "", 2: "" }; + var decoder = new TextDecoder("utf-8"); + + function err(code, message) { + var e = new Error(message || code); + e.code = code; + return e; + } + + function enosys() { + return err("ENOSYS", "not implemented"); + } + + // Go's syscall.setStat reads every one of these fields with `.Int()`, which + // panics on undefined. Returning a partial object kills the wasm instance on + // the first stat, so all thirteen are always present. + function statFor(bytes) { + return { + dev: 0, + ino: 0, + mode: S_IFREG | 0o444, + nlink: 1, + uid: 0, + gid: 0, + rdev: 0, + size: bytes.length, + blksize: 4096, + blocks: Math.ceil(bytes.length / 512), + atimeMs: 0, + mtimeMs: 0, + ctimeMs: 0, + isDirectory: function () { + return false; + }, + }; + } + + function flush(fd) { + var nl = outputBuf[fd].lastIndexOf("\n"); + if (nl === -1) { + return; + } + var line = outputBuf[fd].substring(0, nl); + outputBuf[fd] = outputBuf[fd].substring(nl + 1); + if (fd === 2) { + console.error(line); + } else { + console.log(line); + } + } + + globalThis.fs = { + // The write flags keep wasm_exec.js's -1 sentinels. A read-only open + // passes O_RDONLY (0), so none of these bits are ever set and the -1 + // values are never compared against anything meaningful. + constants: { + O_WRONLY: -1, + O_RDWR: -1, + O_CREAT: -1, + O_TRUNC: -1, + O_APPEND: -1, + O_EXCL: -1, + O_DIRECTORY: -1, + }, + + writeSync: function (fd, buf) { + if (fd !== 1 && fd !== 2) { + throw enosys(); + } + outputBuf[fd] += decoder.decode(buf); + flush(fd); + return buf.length; + }, + + write: function (fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + var n = this.writeSync(fd, buf); + callback(null, n); + }, + + open: function (path, flags, mode, callback) { + var bytes = files.get(path); + if (bytes === undefined) { + callback(err("ENOENT", "no such file or directory: " + path)); + return; + } + var fd = nextFd++; + open.set(fd, { path: path, bytes: bytes, cursor: 0 }); + callback(null, fd); + }, + + close: function (fd, callback) { + open.delete(fd); + callback(null); + }, + + // `syscall.Read` passes position === null, meaning "continue from this + // descriptor's own cursor and advance it". `syscall.Pread` passes a + // number, which reads from that offset and must leave the cursor alone. + // Returning 0 is how EOF is signalled; without it os.ReadFile loops. + read: function (fd, buffer, offset, length, position, callback) { + var f = open.get(fd); + if (f === undefined) { + callback(err("EBADF", "bad file descriptor")); + return; + } + + var start = + position === null || position === undefined ? f.cursor : position; + if (start >= f.bytes.length) { + callback(null, 0, buffer); + return; + } + + var end = Math.min(start + length, f.bytes.length); + var n = end - start; + buffer.set(f.bytes.subarray(start, end), offset); + + if (position === null || position === undefined) { + f.cursor = end; + } + callback(null, n, buffer); + }, + + fstat: function (fd, callback) { + var f = open.get(fd); + if (f === undefined) { + callback(err("EBADF", "bad file descriptor")); + return; + } + callback(null, statFor(f.bytes)); + }, + + stat: function (path, callback) { + var bytes = files.get(path); + if (bytes === undefined) { + callback(err("ENOENT", "no such file or directory: " + path)); + return; + } + callback(null, statFor(bytes)); + }, + + lstat: function (path, callback) { + this.stat(path, callback); + }, + + // Reached only if a stat wrongly reported a directory. ENOTDIR makes that + // mistake obvious instead of looking like a generic unsupported call. + readdir: function (path, callback) { + callback(err("ENOTDIR", "not a directory: " + path)); + }, + + chmod: function (path, mode, callback) { + callback(enosys()); + }, + chown: function (path, uid, gid, callback) { + callback(enosys()); + }, + fchmod: function (fd, mode, callback) { + callback(enosys()); + }, + fchown: function (fd, uid, gid, callback) { + callback(enosys()); + }, + fsync: function (fd, callback) { + callback(null); + }, + ftruncate: function (fd, length, callback) { + callback(enosys()); + }, + lchown: function (path, uid, gid, callback) { + callback(enosys()); + }, + link: function (path, link, callback) { + callback(enosys()); + }, + mkdir: function (path, perm, callback) { + callback(enosys()); + }, + readlink: function (path, callback) { + callback(enosys()); + }, + rename: function (from, to, callback) { + callback(enosys()); + }, + rmdir: function (path, callback) { + callback(enosys()); + }, + symlink: function (path, link, callback) { + callback(enosys()); + }, + truncate: function (path, length, callback) { + callback(enosys()); + }, + unlink: function (path, callback) { + callback(enosys()); + }, + utimes: function (path, atime, mtime, callback) { + callback(enosys()); + }, + }; + + globalThis.paperFS = { + set: function (path, bytes) { + files.set( + path, + bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes), + ); + }, + has: function (path) { + return files.has(path); + }, + clear: function () { + files.clear(); + }, + }; +})(); diff --git a/docs/assets/js/paper-fs.test.mjs b/docs/assets/js/paper-fs.test.mjs new file mode 100644 index 00000000..f0038a40 --- /dev/null +++ b/docs/assets/js/paper-fs.test.mjs @@ -0,0 +1,170 @@ +// Tests for paper-fs.js, the in-memory filesystem Go's js/wasm runtime calls. +// +// node --test docs/assets/js/ +// +// These pin the contract that syscall/fs_js.go actually relies on. Getting any +// of it wrong fails in a way that is easy to miss: the renderer records an +// image-load issue, draws an error box, and still returns a valid PDF, so a +// "did a PDF come out?" check passes with the shim completely broken. +// +// No browser needed — the shim only touches globalThis, so plain node runs it. +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { test, beforeEach } from "node:test"; + +const SOURCE = new URL("./paper-fs.js", import.meta.url); + +// paper-fs.js is a plain script (an IIFE assigning to globalThis), not a module, +// so it is evaluated rather than imported. Re-evaluating resets its state. +function loadShim() { + delete globalThis.fs; + delete globalThis.paperFS; + new Function(readFileSync(SOURCE, "utf8"))(); +} + +// Promisified wrappers over the Node-style callbacks Go uses. +const call = (method, ...args) => + new Promise((resolve, reject) => { + globalThis.fs[method](...args, (err, ...rest) => + err ? reject(err) : resolve(rest.length > 1 ? rest : rest[0]), + ); + }); + +const open = (path) => call("open", path, 0, 0o444); +const close = (fd) => call("close", fd); +const fstat = (fd) => call("fstat", fd); +const read = (fd, buf, offset, length, position) => + call("read", fd, buf, offset, length, position); + +beforeEach(loadShim); + +test("open reports ENOENT for an unregistered path", async () => { + await assert.rejects(open("missing.bin"), (err) => err.code === "ENOENT"); +}); + +test("read and close reject a bad file descriptor", async () => { + await assert.rejects( + read(999, new Uint8Array(4), 0, 4, null), + (err) => err.code === "EBADF", + ); +}); + +test("fstat exposes every field syscall.setStat reads", async () => { + globalThis.paperFS.set("a.bin", new Uint8Array([1, 2, 3])); + const fd = await open("a.bin"); + const stat = await fstat(fd); + + // setStat calls .Int() on each of these; .Int() on undefined panics and takes + // the whole wasm instance with it. + for (const field of [ + "dev", "ino", "mode", "nlink", "uid", "gid", "rdev", "size", + "blksize", "blocks", "atimeMs", "mtimeMs", "ctimeMs", + ]) { + assert.equal(typeof stat[field], "number", `${field} must be a number`); + } + assert.equal(stat.size, 3); + // syscall.Open always follows open with fstat + isDirectory(), and treats a + // true result as a directory it should readdir. + assert.equal(typeof stat.isDirectory, "function"); + assert.equal(stat.isDirectory(), false); + await close(fd); +}); + +test("sequential reads advance the cursor and signal EOF with 0", async () => { + const data = Uint8Array.from({ length: 1000 }, (_, i) => i % 251); + globalThis.paperFS.set("big.bin", data); + + const fd = await open("big.bin"); + const out = new Uint8Array(data.length); + const chunk = new Uint8Array(300); + let total = 0; + + // position === null means "continue from this descriptor's cursor". + for (;;) { + const [n] = await read(fd, chunk, 0, chunk.length, null); + if (n === 0) break; // os.ReadFile loops forever if EOF is never reported + out.set(chunk.subarray(0, n), total); + total += n; + assert.ok(total <= data.length, "read past end of file"); + } + + assert.equal(total, data.length); + assert.deepEqual(out, data); + await close(fd); +}); + +test("a positioned read does not move the cursor", async () => { + const data = Uint8Array.from({ length: 100 }, (_, i) => i); + globalThis.paperFS.set("p.bin", data); + const fd = await open("p.bin"); + + const buf = new Uint8Array(10); + const [n] = await read(fd, buf, 0, 10, 50); // syscall.Pread passes a number + assert.equal(n, 10); + assert.deepEqual(buf, data.subarray(50, 60)); + + // The cursor must still be at 0, so the next cursor-read starts at the top. + const [m] = await read(fd, buf, 0, 10, null); + assert.equal(m, 10); + assert.deepEqual(buf, data.subarray(0, 10)); + await close(fd); +}); + +test("an empty file reads as 0 bytes immediately", async () => { + globalThis.paperFS.set("empty.bin", new Uint8Array([])); + const fd = await open("empty.bin"); + assert.equal((await fstat(fd)).size, 0); + const [n] = await read(fd, new Uint8Array(8), 0, 8, null); + assert.equal(n, 0); + await close(fd); +}); + +test("two descriptors on one file keep independent cursors", async () => { + globalThis.paperFS.set("s.bin", Uint8Array.from({ length: 20 }, (_, i) => i)); + const a = await open("s.bin"); + const b = await open("s.bin"); + assert.notEqual(a, b); + + const buf = new Uint8Array(5); + await read(a, buf, 0, 5, null); // advances a's cursor only + const [n] = await read(b, buf, 0, 5, null); + assert.equal(n, 5); + assert.deepEqual(buf, Uint8Array.from({ length: 5 }, (_, i) => i)); + await close(a); + await close(b); +}); + +test("readdir reports ENOTDIR rather than a generic failure", async () => { + globalThis.paperFS.set("f.bin", new Uint8Array([0])); + await assert.rejects( + call("readdir", "f.bin"), + (err) => err.code === "ENOTDIR", + ); +}); + +test("unsupported operations answer ENOSYS", async () => { + await assert.rejects(call("unlink", "x"), (err) => err.code === "ENOSYS"); + await assert.rejects(call("mkdir", "d", 0o755), (err) => err.code === "ENOSYS"); +}); + +test("stdout and stderr writes are line buffered to the console", () => { + const lines = []; + const realLog = console.log; + console.log = (line) => lines.push(line); + try { + const enc = new TextEncoder(); + globalThis.fs.writeSync(1, enc.encode("hello ")); + assert.deepEqual(lines, [], "nothing flushes until a newline arrives"); + globalThis.fs.writeSync(1, enc.encode("world\n")); + assert.deepEqual(lines, ["hello world"]); + } finally { + console.log = realLog; + } +}); + +test("paperFS.set accepts an ArrayBuffer as well as a Uint8Array", async () => { + globalThis.paperFS.set("ab.bin", new Uint8Array([9, 8, 7]).buffer); + const fd = await open("ab.bin"); + assert.equal((await fstat(fd)).size, 3); + await close(fd); +}); diff --git a/docs/assets/js/paper-preview.js b/docs/assets/js/paper-preview.js new file mode 100644 index 00000000..6f1a5665 --- /dev/null +++ b/docs/assets/js/paper-preview.js @@ -0,0 +1,282 @@ +// paper-preview.js — renders the PDF previews on the docs site. +// +// Two fenced block languages are handled: +// +// ```pdf a path to a committed PDF, e.g. assets/pdf/showcase.pdf +// ```pdf-example the name of a registered example, e.g. imagegrid +// +// The second kind is generated in the browser by paper.wasm from the same +// GetPaper builder the page shows as its code sample, so the preview cannot +// drift from the code beside it. The wasm is ~4.7MB gzipped, so it is fetched +// lazily: a page with no pdf-example fence never requests it, and the +// instantiation is cached for the whole SPA session rather than per route. +// +// This replaces docsify-pdf-embed-plugin, which round-tripped pending embeds +// through localStorage, required $docsify.executeScript, and built absolute +// URLs from location.hostname only — dropping the /paper/ project-pages prefix. +(function () { + "use strict"; + + var WASM_DIR = "assets/wasm/"; + var VIEWER_HEIGHT = "50rem"; + + var wasmReady = null; // cached instantiation promise, shared across routes + var assetCache = new Map(); // repo-relative path -> Uint8Array + var pending = []; // previews awaiting the current render pass + var liveURLs = []; // object URLs to revoke when the next route renders + var seq = 0; + + // Resolve against the document's own URL so the site works under a + // project-pages path prefix such as /paper/. document.baseURI is absolute, + // which URL() requires as a base; the route hash does not affect resolution. + function siteURL(rel) { + return new URL(rel, document.baseURI).href; + } + + function el(tag, className, text) { + var node = document.createElement(tag); + if (className) node.className = className; + if (text) node.textContent = text; + return node; + } + + function loadScript(src) { + return new Promise(function (resolve, reject) { + var s = document.createElement("script"); + s.src = src; + s.onload = resolve; + s.onerror = function () { + reject(new Error("failed to load " + src)); + }; + document.head.appendChild(s); + }); + } + + // instantiateStreaming rejects unless the response Content-Type is exactly + // application/wasm, which plain static file servers do not always send — + // Python's http.server only learned the .wasm mapping in 3.11, and `make site` + // uses it. Stream when we can, fall back to buffering when we cannot, so the + // local flow works anywhere. The playground does the same for the same reason. + async function instantiate(src, importObject) { + var response = await fetch(src); + if (!response.ok) { + throw new Error("fetch " + src + ": HTTP " + response.status); + } + if (WebAssembly.instantiateStreaming) { + try { + return await WebAssembly.instantiateStreaming( + response.clone(), + importObject, + ); + } catch (_) { + // Fall through: almost always a Content-Type the browser refused. + } + } + return WebAssembly.instantiate(await response.arrayBuffer(), importObject); + } + + // paper-fs.js must be evaluated before wasm_exec.js: the latter only installs + // its own stub filesystem when globalThis.fs is absent. + function ensureWasm() { + if (wasmReady) return wasmReady; + + wasmReady = (async function () { + if (!globalThis.paperFS) + await loadScript(siteURL("assets/js/paper-fs.js")); + if (typeof Go === "undefined") + await loadScript(siteURL(WASM_DIR + "wasm_exec.js")); + + var go = new Go(); + var src = siteURL(WASM_DIR + "paper.wasm"); + var result = await instantiate(src, go.importObject); + // main() ends in select{}, so this never resolves; the exports stay + // callable for the lifetime of the page. + go.run(result.instance); + + for ( + var i = 0; + i < 50 && typeof globalThis.paperGenerateExample !== "function"; + i++ + ) { + await new Promise(function (r) { + setTimeout(r, 20); + }); + } + if (typeof globalThis.paperGenerateExample !== "function") { + throw new Error("paper.wasm loaded but registered no exports"); + } + })(); + + // A failed load must not be cached, or every later preview inherits it. + wasmReady.catch(function () { + wasmReady = null; + }); + return wasmReady; + } + + // The map key is the repo-relative path Go passes to os.ReadFile; the URL + // drops the leading "docs/" because the deployed site root is docs/ itself. + async function ensureAssets(paths) { + for (const path of paths) { + if (assetCache.has(path)) { + globalThis.paperFS.set(path, assetCache.get(path)); + continue; + } + var url = siteURL(path.replace(/^docs\//, "")); + var response = await fetch(url); + if (!response.ok) { + throw new Error("fetch " + url + ": HTTP " + response.status); + } + var bytes = new Uint8Array(await response.arrayBuffer()); + assetCache.set(path, bytes); + globalThis.paperFS.set(path, bytes); + } + } + + function unwrap(result, what) { + if (!result || result.error) { + throw new Error( + result && result.error ? result.error : what + " returned nothing", + ); + } + return result; + } + + async function generate(name) { + await ensureWasm(); + var assets = + unwrap(globalThis.paperExampleAssets(name), "paperExampleAssets") + .assets || []; + await ensureAssets(assets); + + var base64 = unwrap( + globalThis.paperGenerateExample(name), + "paperGenerateExample", + ).pdf; + var binary = atob(base64); + var bytes = new Uint8Array(binary.length); + for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return URL.createObjectURL(new Blob([bytes], { type: "application/pdf" })); + } + + // A download link always accompanies the embed. iOS Safari and Android Chrome + // render a blob as a blank box and raise no error, so there is nothing + // to catch and fall back from — the link has to be there unconditionally. + function showPDF(container, url, filename) { + container.textContent = ""; + + var frame = el("embed"); + frame.type = "application/pdf"; + frame.src = url; + frame.style.width = "100%"; + frame.style.height = VIEWER_HEIGHT; + frame.style.border = "0"; + container.appendChild(frame); + + var link = el("a", "paper-preview-download", "Download " + filename); + link.href = url; + link.setAttribute("download", filename); + link.style.display = "inline-block"; + link.style.marginTop = ".5rem"; + container.appendChild(link); + } + + function showError(container, name, message, retry) { + container.textContent = ""; + var box = el("div", "paper-preview-error"); + box.style.border = "1px solid #c00"; + box.style.borderRadius = "4px"; + box.style.padding = "1rem"; + box.appendChild( + el("strong", null, "Could not generate the " + name + " preview"), + ); + var detail = el("pre", null, message); + detail.style.whiteSpace = "pre-wrap"; + detail.style.margin = ".5rem 0"; + box.appendChild(detail); + + var button = el("button", null, "Retry"); + button.type = "button"; + button.addEventListener("click", retry); + box.appendChild(button); + + container.appendChild(box); + } + + function renderExample(container, name) { + container.textContent = ""; + container.appendChild( + el("p", "paper-preview-loading", "Generating " + name + " preview…"), + ); + + generate(name) + .then(function (url) { + liveURLs.push(url); + showPDF(container, url, name + ".pdf"); + }) + .catch(function (err) { + showError( + container, + name, + String(err && err.message ? err.message : err), + function () { + renderExample(container, name); + }, + ); + }); + } + + function renderStatic(container, path) { + var url = siteURL(path); + showPDF(container, url, path.split("/").pop()); + } + + // Docsify's markdown renderer runs before the HTML is in the DOM, so each + // fence only leaves a placeholder here and is wired up in doneEach. + function installRenderer() { + var md = (window.$docsify.markdown = window.$docsify.markdown || {}); + var renderer = (md.renderer = md.renderer || {}); + var previous = renderer.code; + + renderer.code = function (code, lang) { + var kind = + lang === "pdf" ? "static" : lang === "pdf-example" ? "example" : null; + if (!kind) { + // Every other language falls through to whoever had the renderer before + // us, or to docsify's own highlighter via this.origin. + if (previous) return previous.apply(this, arguments); + return this.origin.code.apply(this, arguments); + } + var id = "paper-preview-" + ++seq; + pending.push({ id: id, kind: kind, value: String(code).trim() }); + return '
'; + }; + } + + window.$docsify = window.$docsify || {}; + installRenderer(); + + window.$docsify.plugins = [ + function (hook) { + hook.beforeEach(function (content, next) { + // Docsify never unmounts a route, so blobs from the page we are + // leaving are released here rather than in a teardown hook. + liveURLs.splice(0).forEach(URL.revokeObjectURL); + pending = []; + next(content); + }); + + hook.doneEach(function () { + pending.splice(0).forEach(function (item) { + var container = document.getElementById(item.id); + if (!container) return; + if (item.kind === "static") { + renderStatic(container, item.value); + } else { + renderExample(container, item.value); + } + }); + }); + }, + ].concat(window.$docsify.plugins || []); +})(); diff --git a/docs/assets/pdf/addpage.pdf b/docs/assets/pdf/addpage.pdf deleted file mode 100644 index 2aab5c63..00000000 Binary files a/docs/assets/pdf/addpage.pdf and /dev/null differ diff --git a/docs/assets/pdf/autorow.pdf b/docs/assets/pdf/autorow.pdf deleted file mode 100755 index e2716010..00000000 Binary files a/docs/assets/pdf/autorow.pdf and /dev/null differ diff --git a/docs/assets/pdf/barcodegrid.pdf b/docs/assets/pdf/barcodegrid.pdf deleted file mode 100755 index e9346019..00000000 Binary files a/docs/assets/pdf/barcodegrid.pdf and /dev/null differ diff --git a/docs/assets/pdf/billing.pdf b/docs/assets/pdf/billing.pdf deleted file mode 100644 index 5b91b6a6..00000000 Binary files a/docs/assets/pdf/billing.pdf and /dev/null differ diff --git a/docs/assets/pdf/bookmark.pdf b/docs/assets/pdf/bookmark.pdf deleted file mode 100755 index c61a68d9..00000000 Binary files a/docs/assets/pdf/bookmark.pdf and /dev/null differ diff --git a/docs/assets/pdf/cellstyle.pdf b/docs/assets/pdf/cellstyle.pdf deleted file mode 100644 index e8ac9824..00000000 Binary files a/docs/assets/pdf/cellstyle.pdf and /dev/null differ diff --git a/docs/assets/pdf/checkbox.pdf b/docs/assets/pdf/checkbox.pdf deleted file mode 100755 index de242d8b..00000000 Binary files a/docs/assets/pdf/checkbox.pdf and /dev/null differ diff --git a/docs/assets/pdf/compression.pdf b/docs/assets/pdf/compression.pdf deleted file mode 100644 index b79acf8e..00000000 Binary files a/docs/assets/pdf/compression.pdf and /dev/null differ diff --git a/docs/assets/pdf/customdimensions.pdf b/docs/assets/pdf/customdimensions.pdf deleted file mode 100644 index decb4c88..00000000 Binary files a/docs/assets/pdf/customdimensions.pdf and /dev/null differ diff --git a/docs/assets/pdf/custompage.pdf b/docs/assets/pdf/custompage.pdf deleted file mode 100644 index 1019a441..00000000 Binary files a/docs/assets/pdf/custompage.pdf and /dev/null differ diff --git a/docs/assets/pdf/datamatrixgrid.pdf b/docs/assets/pdf/datamatrixgrid.pdf deleted file mode 100644 index 546fd7f6..00000000 Binary files a/docs/assets/pdf/datamatrixgrid.pdf and /dev/null differ diff --git a/docs/assets/pdf/footer.pdf b/docs/assets/pdf/footer.pdf deleted file mode 100644 index eaeedfa4..00000000 Binary files a/docs/assets/pdf/footer.pdf and /dev/null differ diff --git a/docs/assets/pdf/header.pdf b/docs/assets/pdf/header.pdf deleted file mode 100644 index 38446af6..00000000 Binary files a/docs/assets/pdf/header.pdf and /dev/null differ diff --git a/docs/assets/pdf/imagegrid.pdf b/docs/assets/pdf/imagegrid.pdf deleted file mode 100644 index da6c627b..00000000 Binary files a/docs/assets/pdf/imagegrid.pdf and /dev/null differ diff --git a/docs/assets/pdf/linegrid.pdf b/docs/assets/pdf/linegrid.pdf deleted file mode 100644 index ea3d8481..00000000 Binary files a/docs/assets/pdf/linegrid.pdf and /dev/null differ diff --git a/docs/assets/pdf/list.pdf b/docs/assets/pdf/list.pdf deleted file mode 100644 index 82f4b74f..00000000 Binary files a/docs/assets/pdf/list.pdf and /dev/null differ diff --git a/docs/assets/pdf/lowmemory.pdf b/docs/assets/pdf/lowmemory.pdf deleted file mode 100755 index 2fb07ffa..00000000 Binary files a/docs/assets/pdf/lowmemory.pdf and /dev/null differ diff --git a/docs/assets/pdf/margins.pdf b/docs/assets/pdf/margins.pdf deleted file mode 100644 index 1105d87b..00000000 Binary files a/docs/assets/pdf/margins.pdf and /dev/null differ diff --git a/docs/assets/pdf/maxgridsum.pdf b/docs/assets/pdf/maxgridsum.pdf deleted file mode 100644 index 266f22a4..00000000 Binary files a/docs/assets/pdf/maxgridsum.pdf and /dev/null differ diff --git a/docs/assets/pdf/metadatas.pdf b/docs/assets/pdf/metadatas.pdf deleted file mode 100644 index 2de1a68f..00000000 Binary files a/docs/assets/pdf/metadatas.pdf and /dev/null differ diff --git a/docs/assets/pdf/orientation.pdf b/docs/assets/pdf/orientation.pdf deleted file mode 100644 index c9ea4a3f..00000000 Binary files a/docs/assets/pdf/orientation.pdf and /dev/null differ diff --git a/docs/assets/pdf/pagenumber.pdf b/docs/assets/pdf/pagenumber.pdf deleted file mode 100644 index 71ecee52..00000000 Binary files a/docs/assets/pdf/pagenumber.pdf and /dev/null differ diff --git a/docs/assets/pdf/parallelism.pdf b/docs/assets/pdf/parallelism.pdf deleted file mode 100644 index 3d447489..00000000 Binary files a/docs/assets/pdf/parallelism.pdf and /dev/null differ diff --git a/docs/assets/pdf/protection.pdf b/docs/assets/pdf/protection.pdf deleted file mode 100644 index 100c4eb3..00000000 Binary files a/docs/assets/pdf/protection.pdf and /dev/null differ diff --git a/docs/assets/pdf/qrgrid.pdf b/docs/assets/pdf/qrgrid.pdf deleted file mode 100644 index fc62b46e..00000000 Binary files a/docs/assets/pdf/qrgrid.pdf and /dev/null differ diff --git a/docs/assets/pdf/signaturegrid.pdf b/docs/assets/pdf/signaturegrid.pdf deleted file mode 100644 index bc308283..00000000 Binary files a/docs/assets/pdf/signaturegrid.pdf and /dev/null differ diff --git a/docs/assets/pdf/simplest.pdf b/docs/assets/pdf/simplest.pdf deleted file mode 100755 index cc4a8078..00000000 Binary files a/docs/assets/pdf/simplest.pdf and /dev/null differ diff --git a/docs/assets/pdf/textgrid.pdf b/docs/assets/pdf/textgrid.pdf deleted file mode 100644 index d25676c5..00000000 Binary files a/docs/assets/pdf/textgrid.pdf and /dev/null differ diff --git a/docs/assets/pdf/watermark.pdf b/docs/assets/pdf/watermark.pdf deleted file mode 100755 index ed6afe64..00000000 Binary files a/docs/assets/pdf/watermark.pdf and /dev/null differ diff --git a/docs/docs.html b/docs/docs.html index b5e1be43..f9577b87 100644 --- a/docs/docs.html +++ b/docs/docs.html @@ -69,8 +69,7 @@ - - + diff --git a/docs/examples/billing.md b/docs/examples/billing.md index 6cf5e592..716ad4d5 100644 --- a/docs/examples/billing.md +++ b/docs/examples/billing.md @@ -1,11 +1,11 @@ # Billing ## Code Example -[filename](../assets/examples/billing/main.go ':include :type=code') +[filename](../assets/examples/billing/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/billing.pdf +```pdf-example + billing ``` ## Time Execution diff --git a/docs/examples/simplest.md b/docs/examples/simplest.md index 38df643b..2064af8e 100644 --- a/docs/examples/simplest.md +++ b/docs/examples/simplest.md @@ -1,9 +1,9 @@ # Simplest ## Code Example -[filename](../assets/examples/simplest/main.go ':include :type=code') +[filename](../assets/examples/simplest/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/simplest.pdf +```pdf-example + simplest ``` \ No newline at end of file diff --git a/docs/features/addpage.md b/docs/features/addpage.md index 698848e0..36c55630 100644 --- a/docs/features/addpage.md +++ b/docs/features/addpage.md @@ -10,11 +10,11 @@ This is useful when you want to control pagination explicitly: for example, forc * [props : Page](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/props#Page) ## Code Example -[filename](../assets/examples/addpage/main.go ':include :type=code') +[filename](../assets/examples/addpage/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/addpage.pdf +```pdf-example + addpage ``` ## Time Execution diff --git a/docs/features/autorow.md b/docs/features/autorow.md index 8c934950..a6623155 100644 --- a/docs/features/autorow.md +++ b/docs/features/autorow.md @@ -19,11 +19,11 @@ This is particularly useful for text blocks of unknown length, dynamic lists, or * [image : NewAutoFromFileRow](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/components/image#NewAutoFromFileRow) ## Code Example -[filename](../assets/examples/autorow/main.go ':include :type=code') +[filename](../assets/examples/autorow/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/autorow.pdf +```pdf-example + autorow ``` ## Time Execution @@ -36,11 +36,11 @@ This is particularly useful for text blocks of unknown length, dynamic lists, or * [props : Page](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/props#Page) ## Code Example -[filename](../assets/examples/autorow/main.go ':include :type=code') +[filename](../assets/examples/autorow/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/autorow.pdf +```pdf-example + autorow ``` ## Time Execution diff --git a/docs/features/background.md b/docs/features/background.md index 121f0838..f30f1b3f 100644 --- a/docs/features/background.md +++ b/docs/features/background.md @@ -13,7 +13,7 @@ * [consts : Extension](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/consts/extension#Type) ## Code Example -[filename](../assets/examples/background/main.go ':include :type=code') +[filename](../assets/examples/background/paper.go ':include :type=code') ## PDF Generated ```pdf diff --git a/docs/features/barcode.md b/docs/features/barcode.md index f126255d..e114c9f8 100644 --- a/docs/features/barcode.md +++ b/docs/features/barcode.md @@ -27,11 +27,11 @@ The Barcode component renders a 1-D barcode inside a cell. The default type is ` * [component : Barcode](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/components/code#Barcode) ## Code Example -[filename](../assets/examples/barcodegrid/main.go ':include :type=code') +[filename](../assets/examples/barcodegrid/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/barcodegrid.pdf +```pdf-example + barcodegrid ``` ## Time Execution diff --git a/docs/features/basics.md b/docs/features/basics.md index 3a54a4e3..c091cc19 100644 --- a/docs/features/basics.md +++ b/docs/features/basics.md @@ -32,9 +32,9 @@ The entry point is `paper.New()`, which accepts an optional `*entity.Config` pro * [Checkbox](features/checkbox?id=checkbox) ## Code Example -[filename](../assets/examples/simplest/main.go ':include :type=code') +[filename](../assets/examples/simplest/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/simplest.pdf +```pdf-example + simplest ``` diff --git a/docs/features/bookmark.md b/docs/features/bookmark.md index 09859a29..eba69704 100644 --- a/docs/features/bookmark.md +++ b/docs/features/bookmark.md @@ -29,6 +29,8 @@ m.AddAutoRow(col.New(12).Add(text.New("Installation", props.Text{ }))) ``` -[filename](../assets/examples/bookmark/main.go ':include :type=code') +[filename](../assets/examples/bookmark/paper.go ':include :type=code') -[bookmark.pdf](../assets/pdf/bookmark.pdf ':include :type=pdf') +```pdf-example + bookmark +``` diff --git a/docs/features/cellstyle.md b/docs/features/cellstyle.md index a8dd6541..ea07ecf0 100644 --- a/docs/features/cellstyle.md +++ b/docs/features/cellstyle.md @@ -30,11 +30,11 @@ Cell Style applies visual decoration — background fill, borders, and line styl * [props : Cell](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/props#Cell) ## Code Example -[filename](../assets/examples/cellstyle/main.go ':include :type=code') +[filename](../assets/examples/cellstyle/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/cellstyle.pdf +```pdf-example + cellstyle ``` ## Time Execution diff --git a/docs/features/checkbox.md b/docs/features/checkbox.md index d7256887..26083eff 100644 --- a/docs/features/checkbox.md +++ b/docs/features/checkbox.md @@ -28,11 +28,11 @@ The row height for auto-row usage is `Size + Top`. * [component : Checkbox](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/components/checkbox#Checkbox) ## Code Example -[filename](../assets/examples/checkbox/main.go ':include :type=code') +[filename](../assets/examples/checkbox/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/checkbox.pdf +```pdf-example + checkbox ``` ## Time Execution diff --git a/docs/features/compression.md b/docs/features/compression.md index 8a75f87d..af7d4e38 100644 --- a/docs/features/compression.md +++ b/docs/features/compression.md @@ -14,11 +14,11 @@ Compression is **disabled** by default. Pass `true` to enable it or `false` to e * [builder : WithCompression](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/config#CfgBuilder.WithCompression) ## Code Example -[filename](../assets/examples/compression/main.go ':include :type=code') +[filename](../assets/examples/compression/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/compression.pdf +```pdf-example + compression ``` ## Time Execution [filename](../assets/text/compression.txt ':include :type=code') diff --git a/docs/features/customdimensions.md b/docs/features/customdimensions.md index 5b777aeb..6ca6305a 100644 --- a/docs/features/customdimensions.md +++ b/docs/features/customdimensions.md @@ -12,11 +12,11 @@ * [builder : WithDimensions](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/config#CfgBuilder.WithDimensions) ## Code Example -[filename](../assets/examples/customdimensions/main.go ':include :type=code') +[filename](../assets/examples/customdimensions/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/customdimensions.pdf +```pdf-example + customdimensions ``` ## Time Execution [filename](../assets/text/customdimensions.txt ':include :type=code') diff --git a/docs/features/customfont.md b/docs/features/customfont.md index 4cb59165..523f85ba 100644 --- a/docs/features/customfont.md +++ b/docs/features/customfont.md @@ -15,7 +15,7 @@ Paper ships with a standard set of built-in fonts. `WithCustomFonts` lets you re * [entity : CustomFont](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/core/entity#CustomFont) ## Code Example -[filename](../assets/examples/customfont/main.go ':include :type=code') +[filename](../assets/examples/customfont/paper.go ':include :type=code') ## PDF Generated ```pdf diff --git a/docs/features/custompage.md b/docs/features/custompage.md index 9f171577..82b47fc0 100644 --- a/docs/features/custompage.md +++ b/docs/features/custompage.md @@ -27,11 +27,11 @@ * [pagesize : Type](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/consts/pagesize) ## Code Example -[filename](../assets/examples/custompage/main.go ':include :type=code') +[filename](../assets/examples/custompage/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/custompage.pdf +```pdf-example + custompage ``` ## Time Execution [filename](../assets/text/custompage.txt ':include :type=code') diff --git a/docs/features/datamatrix.md b/docs/features/datamatrix.md index e993c2fd..1dba80cd 100644 --- a/docs/features/datamatrix.md +++ b/docs/features/datamatrix.md @@ -27,11 +27,11 @@ Like QR codes, Data Matrix codes use `props.Rect` for layout control. * [component : MatrixCode](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/components/code#MatrixCode) ## Code Example -[filename](../assets/examples/datamatrixgrid/main.go ':include :type=code') +[filename](../assets/examples/datamatrixgrid/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/datamatrixgrid.pdf +```pdf-example + datamatrixgrid ``` ## Time Execution diff --git a/docs/features/disablepagebreak.md b/docs/features/disablepagebreak.md index a68e9c37..f1776d3e 100644 --- a/docs/features/disablepagebreak.md +++ b/docs/features/disablepagebreak.md @@ -13,7 +13,7 @@ By default, paper automatically inserts a new physical page whenever a row would * [builder : WithDisableAutoPageBreak](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/config#CfgBuilder.WithDisableAutoPageBreak) ## Code Example -[filename](../assets/examples/disablepagebreak/main.go ':include :type=code') +[filename](../assets/examples/disablepagebreak/paper.go ':include :type=code') ## PDF Generated ```pdf diff --git a/docs/features/footer.md b/docs/features/footer.md index 66857a14..0dd6bba6 100644 --- a/docs/features/footer.md +++ b/docs/features/footer.md @@ -13,11 +13,11 @@ * [paper : RegisterFooter](https://pkg.go.dev/github.com/avdoseferovic/paper#Paper.RegisterFooter) ## Code Example -[filename](../assets/examples/footer/main.go ':include :type=code') +[filename](../assets/examples/footer/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/footer.pdf +```pdf-example + footer ``` ## Time Execution diff --git a/docs/features/header.md b/docs/features/header.md index b01ba3f1..03cf111a 100644 --- a/docs/features/header.md +++ b/docs/features/header.md @@ -13,11 +13,11 @@ * [paper : RegisterHeader](https://pkg.go.dev/github.com/avdoseferovic/paper#Paper.RegisterHeader) ## Code Example -[filename](../assets/examples/header/main.go ':include :type=code') +[filename](../assets/examples/header/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/header.pdf +```pdf-example + header ``` ## Time Execution diff --git a/docs/features/image.md b/docs/features/image.md index 11f1cf2f..e823e004 100644 --- a/docs/features/image.md +++ b/docs/features/image.md @@ -32,11 +32,11 @@ Both sources expose the same set of constructors — `New`, `NewCol`, `NewRow`, * [component : FileImage](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/components/image#FileImage) ## Code Example -[filename](../assets/examples/imagegrid/main.go ':include :type=code') +[filename](../assets/examples/imagegrid/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/imagegrid.pdf +```pdf-example + imagegrid ``` ## Time Execution diff --git a/docs/features/line.md b/docs/features/line.md index 479367f2..029b6ff6 100644 --- a/docs/features/line.md +++ b/docs/features/line.md @@ -30,11 +30,11 @@ For auto-row usage, the row height equals the line's `Thickness` value. * [component : Line](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/components/line#Line) ## Code Example -[filename](../assets/examples/line/main.go ':include :type=code') +[filename](../assets/examples/line/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/linegrid.pdf +```pdf-example + line ``` ## Time Execution [filename](../assets/text/linegrid.txt ':include :type=code') diff --git a/docs/features/list.md b/docs/features/list.md index 45f2a718..ae0cb7ad 100644 --- a/docs/features/list.md +++ b/docs/features/list.md @@ -25,11 +25,11 @@ Both functions return `([]core.Row, error)`. Errors: `ErrEmptyArray` (empty slic * [list : BuildFromPointer](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/components/list#BuildFromPointer) ## Code Example -[filename](../assets/examples/list/main.go ':include :type=code') +[filename](../assets/examples/list/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/list.pdf +```pdf-example + list ``` ## Time Execution [filename](../assets/text/list.txt ':include :type=code') diff --git a/docs/features/lowmemory.md b/docs/features/lowmemory.md index a1f6b977..bb45304c 100644 --- a/docs/features/lowmemory.md +++ b/docs/features/lowmemory.md @@ -20,11 +20,11 @@ * [builder : WithSequentialLowMemory](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/config#CfgBuilder.WithSequentialLowMemoryMode) ## Code Example -[filename](../assets/examples/lowmemory/main.go ':include :type=code') +[filename](../assets/examples/lowmemory/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/lowmemory.pdf +```pdf-example + lowmemory ``` ## Time Execution diff --git a/docs/features/margins.md b/docs/features/margins.md index 6133e06f..d1ac9102 100644 --- a/docs/features/margins.md +++ b/docs/features/margins.md @@ -15,11 +15,11 @@ * [builder : WithTopMargin](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/config#CfgBuilder.WithTopMargin) ## Code Example -[filename](../assets/examples/margins/main.go ':include :type=code') +[filename](../assets/examples/margins/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/margins.pdf +```pdf-example + margins ``` ## Time Execution diff --git a/docs/features/maxgridsum.md b/docs/features/maxgridsum.md index 7723e40b..e7a7688e 100644 --- a/docs/features/maxgridsum.md +++ b/docs/features/maxgridsum.md @@ -13,11 +13,11 @@ * [builder : WithMaxGridSize](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/config#CfgBuilder.WithMaxGridSize) ## Code Example -[filename](../assets/examples/maxgridsum/main.go ':include :type=code') +[filename](../assets/examples/maxgridsum/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/maxgridsum.pdf +```pdf-example + maxgridsum ``` ## Time Execution diff --git a/docs/features/mergepdf.md b/docs/features/mergepdf.md index 557852c3..00d2b533 100644 --- a/docs/features/mergepdf.md +++ b/docs/features/mergepdf.md @@ -18,7 +18,15 @@ Paper provides two complementary ways to combine PDF documents: * [pdf : Merge](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/core#Pdf.Merge) ## Code Example -[filename](../assets/examples/mergepdf/main.go ':include :type=code') + +Building the document: + +[filename](../assets/examples/mergepdf/paper.go ':include :type=code') + +Merging another PDF into it before saving — this is the `Document.Merge` call +described above, and it needs the other PDF's bytes: + +[filename](../assets/examples/mergepdf/cmd/main.go ':include :type=code') ## PDF Generated ```pdf diff --git a/docs/features/metadatas.md b/docs/features/metadatas.md index 3255db75..8a28b8bd 100644 --- a/docs/features/metadatas.md +++ b/docs/features/metadatas.md @@ -17,11 +17,11 @@ PDF metadata fields are stored in the document's information dictionary and are * [builder : WithKeywords](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/config#CfgBuilder.WithKeywords) ## Code Example -[filename](../assets/examples/metadatas/main.go ':include :type=code') +[filename](../assets/examples/metadatas/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/metadatas.pdf +```pdf-example + metadatas ``` ## Time Execution diff --git a/docs/features/orientation.md b/docs/features/orientation.md index 76d2ecca..73e26f4d 100644 --- a/docs/features/orientation.md +++ b/docs/features/orientation.md @@ -13,11 +13,11 @@ * [orientation : Type](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/consts/orientation) ## Code Example -[filename](../assets/examples/orientation/main.go ':include :type=code') +[filename](../assets/examples/orientation/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/orientation.pdf +```pdf-example + orientation ``` ## Time Execution diff --git a/docs/features/pagenumber.md b/docs/features/pagenumber.md index 40854250..967653d1 100644 --- a/docs/features/pagenumber.md +++ b/docs/features/pagenumber.md @@ -35,11 +35,11 @@ * [props : Place](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/props#Place) ## Code Example -[filename](../assets/examples/pagenumber/main.go ':include :type=code') +[filename](../assets/examples/pagenumber/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/pagenumber.pdf +```pdf-example + pagenumber ``` ## Time Execution diff --git a/docs/features/parallelism.md b/docs/features/parallelism.md index e6393d7a..38468f9c 100644 --- a/docs/features/parallelism.md +++ b/docs/features/parallelism.md @@ -47,11 +47,11 @@ Splicing is also re-checked while rendering, as defence in depth. If a future fe * [builder : WithParallelPagesMode](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/config#CfgBuilder.WithParallelPagesMode) ## Code Example -[filename](../assets/examples/parallelism/main.go ':include :type=code') +[filename](../assets/examples/parallelism/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/parallelism.pdf +```pdf-example + parallelism ``` ## Time Execution diff --git a/docs/features/protection.md b/docs/features/protection.md index c6b6e750..c43ab116 100644 --- a/docs/features/protection.md +++ b/docs/features/protection.md @@ -48,11 +48,11 @@ cfg := config.NewBuilder(). * [protection : Encryption](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/consts/protection#Encryption) ## Code Example -[filename](../assets/examples/protection/main.go ':include :type=code') +[filename](../assets/examples/protection/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/protection.pdf +```pdf-example + protection ``` ## Time Execution [filename](../assets/text/protection.txt ':include :type=code') diff --git a/docs/features/qrcode.md b/docs/features/qrcode.md index ae0341c8..5cf8eb0e 100644 --- a/docs/features/qrcode.md +++ b/docs/features/qrcode.md @@ -27,11 +27,11 @@ QR codes share the same `props.Rect` struct as images, giving them identical pos * [component : QrCode](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/components/code#QrCode) ## Code Example -[filename](../assets/examples/qrgrid/main.go ':include :type=code') +[filename](../assets/examples/qrgrid/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/qrgrid.pdf +```pdf-example + qrgrid ``` ## Time Execution diff --git a/docs/features/signature.md b/docs/features/signature.md index 29c802eb..7715f46c 100644 --- a/docs/features/signature.md +++ b/docs/features/signature.md @@ -31,11 +31,11 @@ The row height is automatically calculated from the font height plus the `SafePa * [component : Signature](https://pkg.go.dev/github.com/avdoseferovic/paper/pkg/components/signature#Signature) ## Code Example -[filename](../assets/examples/signaturegrid/main.go ':include :type=code') +[filename](../assets/examples/signaturegrid/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/signaturegrid.pdf +```pdf-example + signaturegrid ``` ## Time Execution diff --git a/docs/features/text.md b/docs/features/text.md index 4754ca34..c8e843e9 100644 --- a/docs/features/text.md +++ b/docs/features/text.md @@ -37,11 +37,11 @@ Text can be created as a standalone `Component`, wrapped directly into a `Col`, ## Code Example -[filename](../assets/examples/textgrid/main.go ':include :type=code') +[filename](../assets/examples/textgrid/paper.go ':include :type=code') ## PDF Generated -```pdf - assets/pdf/textgrid.pdf +```pdf-example + textgrid ``` ## Time Execution diff --git a/docs/features/watermark.md b/docs/features/watermark.md index b36541ba..80268495 100644 --- a/docs/features/watermark.md +++ b/docs/features/watermark.md @@ -22,6 +22,8 @@ cfg := config.NewBuilder(). For image-based page backgrounds, see [Background](background.md?id=add-background). -[filename](../assets/examples/watermark/main.go ':include :type=code') +[filename](../assets/examples/watermark/paper.go ':include :type=code') -[watermark.pdf](../assets/pdf/watermark.pdf ':include :type=pdf') +```pdf-example + watermark +``` diff --git a/docs/wasm-support.md b/docs/wasm-support.md index 91f6fc6d..d560fefe 100644 --- a/docs/wasm-support.md +++ b/docs/wasm-support.md @@ -127,6 +127,46 @@ local files is unavailable: The bundled default fonts and inline (`data:`) assets work normally. +## In-browser previews on this site + +The PDF previews on the feature pages are not committed files — they are +generated in your browser, by this wasm module, from the very same `GetPaper` +builder shown above each one as the code sample. A preview therefore cannot +drift from the code beside it. + +A page declares its preview with a fenced block naming a registered example: + +````markdown +```pdf-example + imagegrid +``` +```` + +`docs/assets/js/paper-preview.js` turns that into a preview by calling +`paperExampleAssets(name)`, fetching whatever files the example reads, handing +the bytes to `paperFS`, and then calling `paperGenerateExample(name)`. The +in-memory filesystem in `docs/assets/js/paper-fs.js` is what lets the library's +ordinary `os.ReadFile` calls work, so the example code keeps using the idiomatic +file-based API rather than a browser-specific variant. + +**What it costs.** The module is roughly 4.7MB gzipped. It is fetched lazily on +the first preview and then reused for the rest of the browsing session, so: + +- a page with no preview never downloads it at all; +- moving between pages does not re-download or re-instantiate it; +- the first page with a preview pays the download, and later ones do not. + +Five previews stay committed PDFs (a plain ```` ```pdf ```` fence) because their +inputs are impractical to ship to a browser: **background** and +**disablepagebreak** need a 792KB PNG, **customfont** a 23MB font file, and +**mergepdf** an existing PDF to merge into. **showcase** stays static as a broad +regression fixture, and `paper.pdf` remains in the repository because +`mergepdf` reads it as an input. + +If an example's file cannot be read, generation fails with a visible error and a +retry button rather than rendering a placeholder — the renderer would otherwise +draw an error box into an otherwise valid PDF and the failure would go unnoticed. + ## Notes - The wasm build is guarded in CI (`GOOS=js GOARCH=wasm go build ./...` for both diff --git a/examples/cmd/wasm/README.md b/examples/cmd/wasm/README.md index c2be4a5c..706c5fb9 100644 --- a/examples/cmd/wasm/README.md +++ b/examples/cmd/wasm/README.md @@ -6,14 +6,39 @@ WebAssembly — entirely client-side, no server. A CodeMirror editor on the left selector) drives a preview on the right that re-renders the actual generated PDF as you type. -The Go code (`main.go`, built with `//go:build js && wasm`) registers two globals: +The Go code (`main.go`, built with `//go:build js && wasm`) registers four globals: ```js paperGeneratePDF(html) // HTML → PDF paperGenerateFromSpec(json, pageSize) // component-grid JSON → PDF +paperGenerateExample(name) // a documented example → PDF // each → { pdf: "" } | { error: "" } + +paperExampleAssets(name) // → { assets: [...] } | { error: "..." } ``` +### Rendering a documented example + +`paperGenerateExample` runs the same `GetPaper` builder the docs page for that +example displays, which is how the site generates its PDF previews instead of +committing one per page. Examples that read images need those bytes in place +first, because the browser has no filesystem: + +```js +const { assets } = paperExampleAssets("imagegrid"); +for (const path of assets) { + // The map key is the repo-relative path Go passes to os.ReadFile; the URL + // drops the leading "docs/" because the deployed site root is docs/. + const bytes = await (await fetch(path.replace(/^docs\//, ""))).arrayBuffer(); + paperFS.set(path, new Uint8Array(bytes)); +} +const { pdf } = paperGenerateExample("imagegrid"); +``` + +`paperFS` comes from [`docs/assets/js/paper-fs.js`](../../../docs/assets/js/paper-fs.js), +which must be loaded **before** `wasm_exec.js`. A missing asset is reported as an +error rather than silently rendering a placeholder box. + See [`docs/wasm-support.md`](../../../docs/wasm-support.md) for the JSON spec schema. diff --git a/examples/cmd/wasm/main.go b/examples/cmd/wasm/main.go index 92619bb4..e9dcc12e 100644 --- a/examples/cmd/wasm/main.go +++ b/examples/cmd/wasm/main.go @@ -20,23 +20,26 @@ import ( "github.com/avdoseferovic/paper/examples/internal/wasmconvert" ) -// safeResult runs fn and packages the outcome as a JS object: { pdf: "" } +// safeResult runs fn and packages the outcome as a JS object: { : value } // on success or { error: "" } on failure. A deferred recover() ensures // a panic deep in the render tree becomes an error instead of unwinding past the // js.FuncOf boundary — which would abort the program and permanently disable // every registered callback for the page. -func safeResult(fn func() (string, error)) (result any) { +// +// The success key is a parameter because the exports return different shapes: +// the generators answer with a base64 PDF, paperExampleAssets with a list. +func safeResult(key string, fn func() (any, error)) (result any) { defer func() { if r := recover(); r != nil { result = js.ValueOf(map[string]any{"error": fmt.Sprintf("paper: %v", r)}) } }() - b64, err := fn() + value, err := fn() if err != nil { return js.ValueOf(map[string]any{"error": err.Error()}) } - return js.ValueOf(map[string]any{"pdf": b64}) + return js.ValueOf(map[string]any{key: value}) } // generate is registered as globalThis.paperGeneratePDF(html). @@ -47,7 +50,7 @@ func generate(_ js.Value, args []js.Value) any { }) } html := args[0].String() - return safeResult(func() (string, error) { + return safeResult("pdf", func() (any, error) { return wasmconvert.HTMLToBase64(context.Background(), html) }) } @@ -65,14 +68,58 @@ func generateFromSpec(_ js.Value, args []js.Value) any { if len(args) > 1 && args[1].Type() == js.TypeString { pageSize = args[1].String() } - return safeResult(func() (string, error) { + return safeResult("pdf", func() (any, error) { return wasmconvert.SpecToBase64(context.Background(), spec, pageSize) }) } +// generateExample is registered as globalThis.paperGenerateExample(name). It +// renders one of the documented examples from docs/assets/examples using the +// same GetPaper builder the docs page displays. +// +// Any files the example reads must already be registered with paperFS; call +// paperExampleAssets(name) first to learn which. +func generateExample(_ js.Value, args []js.Value) any { + if len(args) < 1 || args[0].Type() != js.TypeString { + return js.ValueOf(map[string]any{ + "error": "paperGenerateExample(name) requires a single string argument", + }) + } + name := args[0].String() + return safeResult("pdf", func() (any, error) { + return wasmconvert.ExampleToBase64(context.Background(), name) + }) +} + +// exampleAssets is registered as globalThis.paperExampleAssets(name). It returns +// { assets: [...] }: the repo-relative paths the example reads, which the caller +// fetches and hands to paperFS before generating. +func exampleAssets(_ js.Value, args []js.Value) any { + if len(args) < 1 || args[0].Type() != js.TypeString { + return js.ValueOf(map[string]any{ + "error": "paperExampleAssets(name) requires a single string argument", + }) + } + name := args[0].String() + return safeResult("assets", func() (any, error) { + assets, err := wasmconvert.ExampleAssets(name) + if err != nil { + return nil, err + } + // js.ValueOf understands []any, not []string. + out := make([]any, len(assets)) + for i, a := range assets { + out[i] = a + } + return out, nil + }) +} + func main() { js.Global().Set("paperGeneratePDF", js.FuncOf(generate)) js.Global().Set("paperGenerateFromSpec", js.FuncOf(generateFromSpec)) + js.Global().Set("paperGenerateExample", js.FuncOf(generateExample)) + js.Global().Set("paperExampleAssets", js.FuncOf(exampleAssets)) // Block forever so the registered callbacks stay alive for the page. select {} } diff --git a/examples/go.mod b/examples/go.mod index dd61b548..3e3a8542 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -7,7 +7,10 @@ require github.com/avdoseferovic/paper v0.2.1 replace github.com/avdoseferovic/paper => .. require ( + github.com/avdoseferovic/paper/docs v0.0.0 golang.org/x/image v0.43.0 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/text v0.38.0 // indirect ) + +replace github.com/avdoseferovic/paper/docs => ../docs diff --git a/examples/internal/exampleregistry/registry.go b/examples/internal/exampleregistry/registry.go new file mode 100644 index 00000000..ead4bf1f --- /dev/null +++ b/examples/internal/exampleregistry/registry.go @@ -0,0 +1,148 @@ +// Package exampleregistry maps a documented example's name to the builder that +// produces it, so the wasm binary can render any of them on demand for the docs +// site instead of the site shipping a committed PDF per page. +// +// Entries hold the same GetPaper builders the docs pages display and the +// structure tests assert, so a preview cannot drift from the code beside it. +// +// Assets lists every file the builder reads. Those paths are repo-root relative +// because that is exactly the string the library hands to os.ReadFile — on the +// host it resolves against the working directory, and in the browser it is the +// key the in-memory filesystem (docs/assets/js/paper-fs.js) is populated under. +package exampleregistry + +import ( + "errors" + "fmt" + "slices" + + "github.com/avdoseferovic/paper/pkg/core" + + "github.com/avdoseferovic/paper/examples/internal/examplepath" + + "github.com/avdoseferovic/paper/docs/assets/examples/addpage" + "github.com/avdoseferovic/paper/docs/assets/examples/autorow" + "github.com/avdoseferovic/paper/docs/assets/examples/barcodegrid" + "github.com/avdoseferovic/paper/docs/assets/examples/billing" + "github.com/avdoseferovic/paper/docs/assets/examples/bookmark" + "github.com/avdoseferovic/paper/docs/assets/examples/cellstyle" + "github.com/avdoseferovic/paper/docs/assets/examples/checkbox" + "github.com/avdoseferovic/paper/docs/assets/examples/compression" + "github.com/avdoseferovic/paper/docs/assets/examples/customdimensions" + "github.com/avdoseferovic/paper/docs/assets/examples/custompage" + "github.com/avdoseferovic/paper/docs/assets/examples/datamatrixgrid" + "github.com/avdoseferovic/paper/docs/assets/examples/footer" + "github.com/avdoseferovic/paper/docs/assets/examples/header" + "github.com/avdoseferovic/paper/docs/assets/examples/imagegrid" + "github.com/avdoseferovic/paper/docs/assets/examples/line" + "github.com/avdoseferovic/paper/docs/assets/examples/list" + "github.com/avdoseferovic/paper/docs/assets/examples/lowmemory" + "github.com/avdoseferovic/paper/docs/assets/examples/margins" + "github.com/avdoseferovic/paper/docs/assets/examples/maxgridsum" + "github.com/avdoseferovic/paper/docs/assets/examples/metadatas" + "github.com/avdoseferovic/paper/docs/assets/examples/orientation" + "github.com/avdoseferovic/paper/docs/assets/examples/pagenumber" + "github.com/avdoseferovic/paper/docs/assets/examples/parallelism" + "github.com/avdoseferovic/paper/docs/assets/examples/protection" + "github.com/avdoseferovic/paper/docs/assets/examples/qrgrid" + "github.com/avdoseferovic/paper/docs/assets/examples/signaturegrid" + "github.com/avdoseferovic/paper/docs/assets/examples/simplest" + "github.com/avdoseferovic/paper/docs/assets/examples/textgrid" + "github.com/avdoseferovic/paper/docs/assets/examples/watermark" +) + +// ErrUnknownExample is returned by Lookup for a name that is not registered. +var ErrUnknownExample = errors.New("exampleregistry: unknown example") + +// Image assets shared by several examples. +const ( + biplane = "docs/assets/images/biplane.jpg" + frontpage = "docs/assets/images/frontpage.png" + gopherbw = "docs/assets/images/gopherbw.png" +) + +// Example is one documented example that can be rendered on demand. +type Example struct { + // Assets are repo-root-relative paths the builder reads. They must be + // readable before Build runs. + Assets []string + // Build produces the document. It never returns nil. + Build func() core.Paper +} + +// registry holds every example whose preview is generated in the browser. +// +// Four documented examples are deliberately absent because their inputs are too +// large or too awkward to ship to a browser, so their pages keep a committed +// PDF: background and disablepagebreak (a 792KB PNG), customfont (a 23MB TTF) +// and mergepdf (needs an existing PDF to merge). unittests has no builder. +var registry = map[string]Example{ + "addpage": {Build: addpage.GetPaper}, + "autorow": {Assets: []string{biplane}, Build: autorow.GetPaper}, + "barcodegrid": {Build: barcodegrid.GetPaper}, + "billing": {Assets: []string{biplane}, Build: billing.GetPaper}, + "bookmark": {Build: bookmark.GetPaper}, + "cellstyle": {Build: cellstyle.GetPaper}, + "checkbox": {Build: checkbox.GetPaper}, + "compression": {Assets: []string{biplane, frontpage}, Build: compressionPaper}, + "customdimensions": {Assets: []string{biplane}, Build: customdimensions.GetPaper}, + "custompage": {Assets: []string{biplane}, Build: custompage.GetPaper}, + "datamatrixgrid": {Build: datamatrixgrid.GetPaper}, + "footer": {Build: footer.GetPaper}, + "header": {Build: header.GetPaper}, + "imagegrid": {Assets: []string{biplane, frontpage}, Build: imagegrid.GetPaper}, + "line": {Build: line.GetPaper}, + "list": {Build: list.GetPaper}, + "lowmemory": {Build: lowmemory.GetPaper}, + "margins": {Assets: []string{gopherbw}, Build: margins.GetPaper}, + "maxgridsum": {Build: maxgridsum.GetPaper}, + "metadatas": {Build: metadatas.GetPaper}, + "orientation": {Build: orientation.GetPaper}, + "pagenumber": {Build: pagenumber.GetPaper}, + "parallelism": {Build: parallelism.GetPaper}, + "protection": {Build: protection.GetPaper}, + "qrgrid": {Build: qrgrid.GetPaper}, + "signaturegrid": {Build: signaturegrid.GetPaper}, + "simplest": {Assets: []string{biplane}, Build: simplest.GetPaper}, + "textgrid": {Build: textgrid.GetPaper}, + "watermark": {Build: watermark.GetPaper}, +} + +// compressionPaper adapts the one builder that takes an argument and reports an +// error. Unlike the others it reads its image eagerly, at build time rather than +// at render time, so the path has to resolve right now. +// +// examplepath.Repo makes that work on both sides: on the host it anchors the +// path to the repository root, which a test running in this package's directory +// would otherwise miss, and under wasm os.Getwd fails so it hands back the bare +// relative path — precisely the key the in-memory filesystem is populated under. +// +// A failure here means a declared asset was not made readable first, which is a +// wiring bug rather than something a caller can act on, so it panics and lets +// the wasm boundary turn it into an error result. +func compressionPaper() core.Paper { + doc, err := compression.GetPaper(examplepath.Repo(frontpage)) + if err != nil { + panic(fmt.Sprintf("exampleregistry: compression: %v", err)) + } + return doc +} + +// Names returns every registered example name, sorted. +func Names() []string { + names := make([]string, 0, len(registry)) + for name := range registry { + names = append(names, name) + } + slices.Sort(names) + return names +} + +// Lookup returns the example registered under name, or ErrUnknownExample. +func Lookup(name string) (Example, error) { + entry, ok := registry[name] + if !ok { + return Example{}, fmt.Errorf("%w: %q", ErrUnknownExample, name) + } + return entry, nil +} diff --git a/examples/internal/exampleregistry/registry_test.go b/examples/internal/exampleregistry/registry_test.go new file mode 100644 index 00000000..78ef5d5f --- /dev/null +++ b/examples/internal/exampleregistry/registry_test.go @@ -0,0 +1,165 @@ +package exampleregistry_test + +import ( + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "slices" + "strconv" + "strings" + "testing" + + "github.com/avdoseferovic/paper/examples/internal/examplepath" + "github.com/avdoseferovic/paper/examples/internal/exampleregistry" +) + +// wantNames is every documented example whose PDF preview is generated in the +// browser. The four whose inputs are too awkward to ship (background and +// disablepagebreak need a 792KB PNG, customfont a 23MB TTF, mergepdf an existing +// PDF) stay static fixtures and are deliberately absent, as is unittests, which +// has no builder. +var wantNames = []string{ + "addpage", "autorow", "barcodegrid", "billing", "bookmark", "cellstyle", + "checkbox", "compression", "customdimensions", "custompage", "datamatrixgrid", + "footer", "header", "imagegrid", "line", "list", "lowmemory", "margins", + "maxgridsum", "metadatas", "orientation", "pagenumber", "parallelism", + "protection", "qrgrid", "signaturegrid", "simplest", "textgrid", "watermark", +} + +func TestNamesAreExactAndSorted(t *testing.T) { + t.Parallel() + + got := exampleregistry.Names() + + if !slices.Equal(got, wantNames) { + t.Errorf("Names() mismatch:\n got: %v\nwant: %v", got, wantNames) + } + if !slices.IsSorted(got) { + t.Error("Names() must be sorted for deterministic output") + } +} + +func TestStaticFixturesAreNotRegistered(t *testing.T) { + t.Parallel() + + for _, name := range []string{"background", "customfont", "disablepagebreak", "mergepdf", "unittests"} { + if _, err := exampleregistry.Lookup(name); err == nil { + t.Errorf("%q must not be registered: it stays a static fixture", name) + } + } +} + +func TestLookupUnknownReturnsError(t *testing.T) { + t.Parallel() + + if _, err := exampleregistry.Lookup("nope"); err == nil { + t.Error("expected an error for an unknown example, got nil") + } +} + +func TestEveryEntryBuildsAndDeclaresRealAssets(t *testing.T) { + t.Parallel() + + for _, name := range exampleregistry.Names() { + t.Run(name, func(t *testing.T) { + t.Parallel() + + entry, err := exampleregistry.Lookup(name) + if err != nil { + t.Fatalf("Lookup(%q): %v", name, err) + } + + // Asset paths are repo-root relative because that is the string the + // library's os.ReadFile receives, both here and behind the browser's + // in-memory filesystem. Tests run with cwd set to this package, so + // they need examplepath.Repo to find the real file. + for _, asset := range entry.Assets { + if _, err := os.Stat(examplepath.Repo(asset)); err != nil { + t.Errorf("declared asset %q does not exist: %v", asset, err) + } + } + + if doc := entry.Build(); doc == nil { + t.Error("Build() returned nil") + } + }) + } +} + +// TestDeclaredAssetsCoverSource parses each registered example's paper.go and +// checks that every image path hardcoded there is also declared in the registry. +// Without this, adding an image to an example silently breaks its browser +// preview: nothing fetches the file, the read fails, and the library quietly +// draws an error box instead of returning an error. +// +// The check is containment rather than equality because a builder can also +// receive a path as an argument that the registry supplies — compression takes +// its frontpage.png that way, so that path is declared but appears in no literal +// inside paper.go. +func TestDeclaredAssetsCoverSource(t *testing.T) { + t.Parallel() + + for _, name := range exampleregistry.Names() { + t.Run(name, func(t *testing.T) { + t.Parallel() + + entry, err := exampleregistry.Lookup(name) + if err != nil { + t.Fatalf("Lookup(%q): %v", name, err) + } + + src := examplepath.Repo(filepath.Join("docs", "assets", "examples", name, "paper.go")) + for _, found := range imagePathsIn(t, src) { + if !slices.Contains(entry.Assets, found) { + t.Errorf("%s reads %q but the registry does not declare it (declared: %v)", + src, found, entry.Assets) + } + } + }) + } +} + +// imagePathsIn returns the distinct string literals passed as the path argument +// to the image.NewFromFile* constructors in the given file. +func imagePathsIn(t *testing.T, path string) []string { + t.Helper() + + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, path, nil, 0) + if err != nil { + t.Fatalf("parse %s: %v", path, err) + } + + var out []string + ast.Inspect(file, func(n ast.Node) bool { + call, ok := n.(*ast.CallExpr) + if !ok { + return true + } + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok || !strings.HasPrefix(sel.Sel.Name, "NewFromFile") && !strings.HasPrefix(sel.Sel.Name, "NewAutoFromFile") { + return true + } + pkg, ok := sel.X.(*ast.Ident) + if !ok || pkg.Name != "image" { + return true + } + for _, arg := range call.Args { + lit, ok := arg.(*ast.BasicLit) + if !ok || lit.Kind != token.STRING { + continue + } + value, err := strconv.Unquote(lit.Value) + if err != nil || !strings.HasPrefix(value, "docs/assets/") { + continue + } + if !slices.Contains(out, value) { + out = append(out, value) + } + } + return true + }) + return out +} diff --git a/examples/internal/wasmconvert/example.go b/examples/internal/wasmconvert/example.go new file mode 100644 index 00000000..96472928 --- /dev/null +++ b/examples/internal/wasmconvert/example.go @@ -0,0 +1,92 @@ +package wasmconvert + +import ( + "context" + "errors" + "fmt" + "slices" + + "github.com/avdoseferovic/paper/pkg/metrics" + + "github.com/avdoseferovic/paper/examples/internal/exampleregistry" +) + +// ErrUnknownExample is returned when the requested example is not registered. +var ErrUnknownExample = errors.New("wasmconvert: unknown example") + +// errExamplePanic wraps a recovered panic from an example's render tree. +var errExamplePanic = errors.New("wasmconvert: example render panicked") + +// ExampleAssets returns the repo-relative paths the named example reads. The +// caller is expected to make each one readable — in the browser, by fetching it +// and handing the bytes to paperFS — before calling ExampleToBase64. +func ExampleAssets(name string) ([]string, error) { + entry, err := exampleregistry.Lookup(name) + if err != nil { + return nil, unknownExample(name, err) + } + // Clone: the slice shares its backing array with the package-level registry, + // so handing it out directly would let a caller rewrite global state. + return slices.Clone(entry.Assets), nil +} + +// unknownExample translates a registry lookup failure into this package's +// sentinel while keeping the original error in the chain, so errors.Is matches +// either sentinel and a future non-not-found error is not mislabelled. +func unknownExample(name string, err error) error { + if errors.Is(err, exampleregistry.ErrUnknownExample) { + return fmt.Errorf("%w: %q: %w", ErrUnknownExample, name, err) + } + return fmt.Errorf("wasmconvert: look up %q: %w", name, err) +} + +// ExampleToBase64 renders the named documented example and returns the PDF as +// base64. It recovers from any panic in the render tree so the caller — and the +// wasm goroutine — always survives. +func ExampleToBase64(ctx context.Context, name string) (b64 string, err error) { + defer func() { + if r := recover(); r != nil { + b64 = "" + err = fmt.Errorf("%w: %v", errExamplePanic, r) + } + }() + + entry, lookupErr := exampleregistry.Lookup(name) + if lookupErr != nil { + return "", unknownExample(name, lookupErr) + } + + pdf, genErr := entry.Build().Generate(ctx) + if genErr != nil { + return "", fmt.Errorf("wasmconvert: generate %s: %w", name, genErr) + } + + // A file the renderer could not read is not a generation error: the provider + // records a render issue, draws an error box in the cell and carries on, so a + // perfectly valid PDF comes back with a placeholder where the image should + // be. Surfacing it here is what keeps a broken asset from shipping silently. + if issue, ok := firstAssetIssue(pdf); ok { + return "", fmt.Errorf("wasmconvert: %s: %s", name, issue) + } + + return pdf.GetBase64(), nil +} + +// firstAssetIssue reports the first recorded issue that means an input file was +// not readable, along with whether one was found. +func firstAssetIssue(pdf interface{ GetReport() *metrics.Report }) (string, bool) { + report := pdf.GetReport() + if report == nil { + return "", false + } + for _, issue := range report.RenderIssues { + if issue.Operation != "image.load" { + continue + } + if issue.Error != "" { + return fmt.Sprintf("%s: %s (%s)", issue.Operation, issue.Message, issue.Error), true + } + return fmt.Sprintf("%s: %s", issue.Operation, issue.Message), true + } + return "", false +} diff --git a/examples/internal/wasmconvert/example_test.go b/examples/internal/wasmconvert/example_test.go new file mode 100644 index 00000000..d6abc01a --- /dev/null +++ b/examples/internal/wasmconvert/example_test.go @@ -0,0 +1,149 @@ +package wasmconvert_test + +import ( + "context" + "encoding/base64" + "errors" + "strings" + "testing" + + "github.com/avdoseferovic/paper/examples/internal/examplepath" + "github.com/avdoseferovic/paper/examples/internal/exampleregistry" + "github.com/avdoseferovic/paper/examples/internal/wasmconvert" +) + +// TestExampleToBase64RendersEveryExample generates all 29 registered examples on +// the host. It is the cheapest regression net for the previews that no longer +// ship as committed PDFs. +// +// It cannot run in parallel: the examples reference their images by repo-root +// relative path (unchanged, because the browser resolves those same strings +// through the in-memory filesystem), so the test has to run from the repo root, +// and t.Chdir is process-wide. +func TestExampleToBase64RendersEveryExample(t *testing.T) { + t.Chdir(examplepath.Repo(".")) + + for _, name := range exampleregistry.Names() { + t.Run(name, func(t *testing.T) { + b64, err := wasmconvert.ExampleToBase64(context.Background(), name) + if err != nil { + t.Fatalf("ExampleToBase64(%q): %v", name, err) + } + + raw, decodeErr := base64.StdEncoding.DecodeString(b64) + if decodeErr != nil { + t.Fatalf("result is not valid base64: %v", decodeErr) + } + if !strings.HasPrefix(string(raw), "%PDF-") { + t.Errorf("decoded output is not a PDF, starts with %q", firstBytes(raw)) + } + }) + } +} + +// TestExampleToBase64ReportsUnreadableAsset pins the behaviour that makes the +// suite above meaningful. A missing image does not fail generation: the provider +// records a render issue, draws an error box and returns a perfectly valid PDF. +// Checking only for a %PDF- prefix would therefore pass even if no image ever +// loaded, so ExampleToBase64 turns that recorded issue into an error. +func TestExampleToBase64ReportsUnreadableAsset(t *testing.T) { + t.Chdir(t.TempDir()) // nothing readable here, so every image lookup fails + + _, err := wasmconvert.ExampleToBase64(context.Background(), "imagegrid") + if err == nil { + t.Fatal("expected an error when the example's images are unreadable, got nil") + } + if !strings.Contains(err.Error(), "image.load") { + t.Errorf("error should name the failing operation, got: %v", err) + } +} + +func TestExampleToBase64UnknownName(t *testing.T) { + t.Parallel() + + _, err := wasmconvert.ExampleToBase64(context.Background(), "nope") + if !errors.Is(err, wasmconvert.ErrUnknownExample) { + t.Errorf("expected ErrUnknownExample, got %v", err) + } + // The registry's own sentinel stays in the chain, so a caller can match + // either one and a future non-not-found failure cannot be mislabelled. + if !errors.Is(err, exampleregistry.ErrUnknownExample) { + t.Errorf("registry sentinel should remain in the chain, got %v", err) + } +} + +// TestExampleToBase64RecoversPanic covers the deferred recover(). A builder can +// genuinely panic — compressionPaper does so by design when its image was not +// made readable first — and a panic crossing the js.FuncOf boundary would abort +// the program and permanently disable every export on the page. +func TestExampleToBase64RecoversPanic(t *testing.T) { + t.Chdir(t.TempDir()) // compression reads its image eagerly, so this panics + + b64, err := wasmconvert.ExampleToBase64(context.Background(), "compression") + + if err == nil { + t.Fatal("expected an error from the panicking builder, got nil") + } + if b64 != "" { + t.Errorf("expected no output alongside the error, got %d bytes", len(b64)) + } + if !strings.Contains(err.Error(), "panicked") { + t.Errorf("error should identify the panic, got: %v", err) + } + + // The process is still usable afterwards — that is the whole point. + if _, err := wasmconvert.ExampleAssets("textgrid"); err != nil { + t.Errorf("package unusable after recovering a panic: %v", err) + } +} + +// TestExampleAssetsDoesNotAliasRegistry pins that callers get a copy. The slice +// would otherwise share its backing array with the package-level registry map. +func TestExampleAssetsDoesNotAliasRegistry(t *testing.T) { + t.Parallel() + + first, err := wasmconvert.ExampleAssets("imagegrid") + if err != nil { + t.Fatalf("ExampleAssets: %v", err) + } + first[0] = "clobbered" + + second, err := wasmconvert.ExampleAssets("imagegrid") + if err != nil { + t.Fatalf("ExampleAssets: %v", err) + } + if second[0] == "clobbered" { + t.Error("mutating the returned slice corrupted the registry") + } +} + +func TestExampleAssets(t *testing.T) { + t.Parallel() + + assets, err := wasmconvert.ExampleAssets("imagegrid") + if err != nil { + t.Fatalf("ExampleAssets: %v", err) + } + if len(assets) != 2 { + t.Errorf("imagegrid should declare 2 images, got %v", assets) + } + + empty, err := wasmconvert.ExampleAssets("textgrid") + if err != nil { + t.Fatalf("ExampleAssets: %v", err) + } + if len(empty) != 0 { + t.Errorf("textgrid needs no assets, got %v", empty) + } + + if _, err := wasmconvert.ExampleAssets("nope"); !errors.Is(err, wasmconvert.ErrUnknownExample) { + t.Errorf("expected ErrUnknownExample, got %v", err) + } +} + +func firstBytes(b []byte) string { + if len(b) > 8 { + b = b[:8] + } + return string(b) +} diff --git a/test/paper/examples/bookmark.json b/test/paper/examples/bookmark.json new file mode 100644 index 00000000..425d32c1 --- /dev/null +++ b/test/paper/examples/bookmark.json @@ -0,0 +1,155 @@ +{ + "type": "paper", + "details": { + "chunk_workers": 1, + "config_margin_bottom": 20.0025, + "config_margin_left": 10, + "config_margin_right": 10, + "config_margin_top": 10, + "config_max_grid_sum": 12, + "config_provider_type": "paper", + "generation_mode": "sequential", + "paper_dimension_height": 297, + "paper_dimension_width": 210, + "prop_font_color": "RGB(0, 0, 0)", + "prop_font_family": "arial", + "prop_font_size": 10 + }, + "nodes": [ + { + "type": "page", + "nodes": [ + { + "value": 5.644444444444445, + "type": "row", + "nodes": [ + { + "value": 12, + "type": "col", + "nodes": [ + { + "value": "Introduction", + "type": "text", + "details": { + "prop_align": "L", + "prop_breakline_strategy": "empty_space_strategy", + "prop_color": "RGB(0, 0, 0)", + "prop_font_family": "arial", + "prop_font_size": 16, + "prop_outline_level": 0 + } + } + ] + } + ] + }, + { + "value": 3.527777777777778, + "type": "row", + "nodes": [ + { + "value": 12, + "type": "col", + "nodes": [ + { + "value": "Some introductory prose.", + "type": "text", + "details": { + "prop_align": "L", + "prop_breakline_strategy": "empty_space_strategy", + "prop_color": "RGB(0, 0, 0)", + "prop_font_family": "arial", + "prop_font_size": 10 + } + } + ] + } + ] + }, + { + "value": 5.644444444444445, + "type": "row", + "nodes": [ + { + "value": 12, + "type": "col", + "nodes": [ + { + "value": "Getting Started", + "type": "text", + "details": { + "prop_align": "L", + "prop_breakline_strategy": "empty_space_strategy", + "prop_color": "RGB(0, 0, 0)", + "prop_font_family": "arial", + "prop_font_size": 16, + "prop_outline_level": 0 + } + } + ] + } + ] + }, + { + "value": 4.233333333333333, + "type": "row", + "nodes": [ + { + "value": 12, + "type": "col", + "nodes": [ + { + "value": "Installation", + "type": "text", + "details": { + "prop_align": "L", + "prop_breakline_strategy": "empty_space_strategy", + "prop_color": "RGB(0, 0, 0)", + "prop_font_family": "arial", + "prop_font_size": 12, + "prop_outline_level": 1 + } + } + ] + } + ] + }, + { + "value": 4.233333333333333, + "type": "row", + "nodes": [ + { + "value": 12, + "type": "col", + "nodes": [ + { + "value": "First Document", + "type": "text", + "details": { + "prop_align": "L", + "prop_breakline_strategy": "empty_space_strategy", + "prop_color": "RGB(0, 0, 0)", + "prop_font_family": "arial", + "prop_font_size": 12, + "prop_outline_level": 1, + "prop_outline_title": "Your first document" + } + } + ] + } + ] + }, + { + "value": 243.714166666, + "type": "row", + "nodes": [ + { + "value": 12, + "type": "col" + } + ] + } + ] + } + ] +} diff --git a/test/paper/examples/watermark.json b/test/paper/examples/watermark.json new file mode 100644 index 00000000..424fee5f --- /dev/null +++ b/test/paper/examples/watermark.json @@ -0,0 +1,99 @@ +{ + "type": "paper", + "details": { + "chunk_workers": 1, + "config_margin_bottom": 20.0025, + "config_margin_left": 10, + "config_margin_right": 10, + "config_margin_top": 10, + "config_max_grid_sum": 12, + "config_provider_type": "paper", + "config_watermark": "DRAFT", + "generation_mode": "sequential", + "paper_dimension_height": 297, + "paper_dimension_width": 210, + "prop_font_color": "RGB(0, 0, 0)", + "prop_font_family": "arial", + "prop_font_size": 10 + }, + "nodes": [ + { + "type": "page", + "nodes": [ + { + "value": 250, + "type": "row", + "nodes": [ + { + "value": 12, + "type": "col", + "nodes": [ + { + "value": "Body content flows over the watermark.", + "type": "text", + "details": { + "prop_align": "L", + "prop_breakline_strategy": "empty_space_strategy", + "prop_color": "RGB(0, 0, 0)", + "prop_font_family": "arial", + "prop_font_size": 10, + "prop_top": 5 + } + } + ] + } + ] + }, + { + "value": 16.9975, + "type": "row", + "nodes": [ + { + "value": 12, + "type": "col" + } + ] + } + ] + }, + { + "type": "page", + "nodes": [ + { + "value": 250, + "type": "row", + "nodes": [ + { + "value": 12, + "type": "col", + "nodes": [ + { + "value": "Body content flows over the watermark.", + "type": "text", + "details": { + "prop_align": "L", + "prop_breakline_strategy": "empty_space_strategy", + "prop_color": "RGB(0, 0, 0)", + "prop_font_family": "arial", + "prop_font_size": 10, + "prop_top": 5 + } + } + ] + } + ] + }, + { + "value": 16.9975, + "type": "row", + "nodes": [ + { + "value": 12, + "type": "col" + } + ] + } + ] + } + ] +}