initial commit: vibe coded compiler
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
dist/
|
||||
|
||||
40
Makefile
Normal file
40
Makefile
Normal file
@@ -0,0 +1,40 @@
|
||||
GO := $(shell which go)
|
||||
ifeq (, $(GO))
|
||||
$(error "Required Go toolchain (go) not in $(PATH).")
|
||||
endif
|
||||
|
||||
PAGES := $(sort $(wildcard pages/*.html)) pages.yaml
|
||||
|
||||
.PHONY: serve build clean
|
||||
|
||||
build: dist/index.stamp
|
||||
|
||||
dist/index.stamp: $(PAGES) templates/base.gohtml templates/index.gohtml templates/print.gohtml templates/print_2up.gohtml cmd/build/main.go
|
||||
@$(GO) run ./cmd/build
|
||||
@mkdir -p dist
|
||||
@date +%s > dist/index.stamp
|
||||
|
||||
serve: build
|
||||
xdg-open "http://localhost:8000" || true
|
||||
python -m http.server
|
||||
|
||||
PDF := dist/output.pdf
|
||||
PDF_2UP := dist/output-2up.pdf
|
||||
|
||||
.PHONY: deps
|
||||
deps:
|
||||
@$(GO) mod tidy
|
||||
|
||||
.PHONY: pdf
|
||||
pdf: build deps
|
||||
@echo "Generating PDF with headless Chrome..."
|
||||
@$(GO) run ./cmd/pdf --in dist/print.html --out $(PDF)
|
||||
|
||||
.PHONY: pdf-2up
|
||||
pdf-2up: build deps
|
||||
@echo "Generating 2-up PDF with headless Chrome..."
|
||||
@$(GO) run ./cmd/pdf --in dist/print_2up.html --out $(PDF_2UP) --w 11 --h 8.5
|
||||
|
||||
clean:
|
||||
rm -rf dist index.html
|
||||
|
||||
93
cmd/pdf/main.go
Normal file
93
cmd/pdf/main.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"smartbar/internal/config"
|
||||
|
||||
"github.com/chromedp/cdproto/emulation"
|
||||
"github.com/chromedp/cdproto/page"
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
func findChromeExec() (string, error) {
|
||||
// Honor CHROME_PATH if set
|
||||
if v := os.Getenv("CHROME_PATH"); v != "" {
|
||||
return v, nil
|
||||
}
|
||||
candidates := []string{
|
||||
"google-chrome-stable",
|
||||
"google-chrome",
|
||||
"chromium-browser",
|
||||
"chromium",
|
||||
"chrome",
|
||||
}
|
||||
for _, name := range candidates {
|
||||
if p, err := exec.LookPath(name); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no Chrome/Chromium executable found; set CHROME_PATH or install chromium/google-chrome")
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
input string
|
||||
output string
|
||||
width float64
|
||||
height float64
|
||||
)
|
||||
flag.StringVar(&input, "in", "dist/print.html", "input HTML file path")
|
||||
flag.StringVar(&output, "out", "dist/output.pdf", "output PDF path")
|
||||
flag.Float64Var(&width, "w", config.PageWidthIn, "page width in inches")
|
||||
flag.Float64Var(&height, "h", config.PageHeightIn, "page height in inches")
|
||||
flag.Parse()
|
||||
|
||||
absInput, _ := filepath.Abs(input)
|
||||
url := "file://" + absInput
|
||||
|
||||
execPath, err := findChromeExec()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.ExecPath(execPath), chromedp.Flag("headless", true), chromedp.Flag("disable-gpu", true), chromedp.Flag("hide-scrollbars", true))
|
||||
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
defer cancel()
|
||||
ctx, cancel := chromedp.NewContext(allocCtx)
|
||||
defer cancel()
|
||||
|
||||
if err := chromedp.Run(ctx, chromedp.Tasks{
|
||||
chromedp.Navigate(url),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
// Ensure media is print for layout (API style for v0.9.x)
|
||||
if err := emulation.SetEmulatedMedia().WithMedia("print").Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
// Print to PDF with exact dimensions and zero margins
|
||||
wIn := width
|
||||
hIn := height
|
||||
params := page.PrintToPDF().
|
||||
WithPaperWidth(wIn).
|
||||
WithPaperHeight(hIn).
|
||||
WithMarginTop(0).WithMarginBottom(0).WithMarginLeft(0).WithMarginRight(0).
|
||||
WithPrintBackground(true)
|
||||
buf, _, err := params.Do(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(output, buf, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Wrote %s\n", output)
|
||||
return nil
|
||||
}),
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
69
css/style.css
Normal file
69
css/style.css
Normal file
@@ -0,0 +1,69 @@
|
||||
@page {
|
||||
/* Browsers may ignore CSS vars in @page; PDF size is controlled by generator */
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Base */
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Use border-box sizing everywhere to keep dimensions predictable */
|
||||
html { box-sizing: border-box; }
|
||||
*, *::before, *::after { box-sizing: inherit; }
|
||||
|
||||
/* Screen preview: render a page at exact CSS inches and center it */
|
||||
@media screen {
|
||||
body {
|
||||
background: #eeeeee;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#page {
|
||||
width: var(--page-w, 5.5in);
|
||||
height: var(--page-h, 8.5in);
|
||||
background: white;
|
||||
box-shadow: 0 0 0 1px rgba(0,0,0,0.08), 0 8px 24px rgba(0,0,0,0.18);
|
||||
}
|
||||
}
|
||||
|
||||
/* Print-specific sizing to ensure exact physical dimensions */
|
||||
@media print {
|
||||
html, body {
|
||||
width: var(--page-w, 5.5in);
|
||||
height: var(--page-h, 8.5in);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden; /* Never spill to a second page */
|
||||
}
|
||||
|
||||
#page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: var(--page-w, 5.5in);
|
||||
max-height: var(--page-h, 8.5in);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden; /* Clip any overflow just in case */
|
||||
page-break-inside: avoid; /* Legacy */
|
||||
break-inside: avoid-page; /* Modern */
|
||||
}
|
||||
|
||||
/* Avoid browser-added headers/footers if possible (user may need to disable in print dialog) */
|
||||
body {
|
||||
-webkit-print-color-adjust: exact;
|
||||
print-color-adjust: exact;
|
||||
box-sizing: border-box;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
18
go.mod
Normal file
18
go.mod
Normal file
@@ -0,0 +1,18 @@
|
||||
module smartbar
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732
|
||||
github.com/chromedp/chromedp v0.9.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.3.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
)
|
||||
23
go.sum
Normal file
23
go.sum
Normal file
@@ -0,0 +1,23 @@
|
||||
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732 h1:XYUCaZrW8ckGWlCRJKCSoh/iFwlpX316a8yY9IFEzv8=
|
||||
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg=
|
||||
github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y=
|
||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
|
||||
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
34
index.html
Normal file
34
index.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>All Pages</title>
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; }
|
||||
body { background: #eeeeee; }
|
||||
.main { display: flex; flex-direction: column; align-items: center; gap: 1rem; padding: 1rem; box-sizing: border-box; }
|
||||
.thumb { display: flex; flex-direction: column; align-items: center; }
|
||||
.label { font: 12px/1.2 -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif; color: #555; margin: 0.25rem 0; }
|
||||
.thumb iframe { width: 5.5in; height: 8.5in; border: 0; background: white; box-shadow: 0 0 0 1px rgba(0,0,0,0.08), 0 8px 24px rgba(0,0,0,0.18); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main">
|
||||
|
||||
<div class="thumb">
|
||||
<div class="label">cover.html</div>
|
||||
<iframe src="/dist/cover.html" loading="lazy"></iframe>
|
||||
</div>
|
||||
|
||||
<div class="thumb">
|
||||
<div class="label">bouba.html</div>
|
||||
<iframe src="/dist/bouba.html" loading="lazy"></iframe>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
7
internal/config/config.go
Normal file
7
internal/config/config.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
// Centralized physical page size in inches. Change here to retarget the whole system.
|
||||
var (
|
||||
PageWidthIn = 5.5
|
||||
PageHeightIn = 8.5
|
||||
)
|
||||
5
pages.yaml
Normal file
5
pages.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
# locations in pages/
|
||||
|
||||
- cover.html
|
||||
- bouba.html
|
||||
|
||||
9
pages/bouba.html
Normal file
9
pages/bouba.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<style>
|
||||
#page {
|
||||
background-color: red;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h1>bouba</h1>
|
||||
|
||||
This is a test kiki.
|
||||
8
pages/cover.html
Normal file
8
pages/cover.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<style>
|
||||
#page {
|
||||
background-color: green;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h1>Smart Bar</h1>
|
||||
welcome to smart bar
|
||||
14
templates/base.gohtml
Normal file
14
templates/base.gohtml
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{ .Title }}</title>
|
||||
<link rel="stylesheet" href="../css/style.css" />
|
||||
</head>
|
||||
<body {{ .PageSizeAttr }}>
|
||||
<div id="page">{{ .Content }}</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
29
templates/index.gohtml
Normal file
29
templates/index.gohtml
Normal file
@@ -0,0 +1,29 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>All Pages</title>
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; }
|
||||
body { background: #eeeeee; }
|
||||
.main { display: flex; flex-direction: column; align-items: center; gap: 1rem; padding: 1rem; box-sizing: border-box; }
|
||||
.thumb { display: flex; flex-direction: column; align-items: center; }
|
||||
.label { font: 12px/1.2 -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif; color: #555; margin: 0.25rem 0; }
|
||||
.thumb iframe { width: 5.5in; height: 8.5in; border: 0; background: white; box-shadow: 0 0 0 1px rgba(0,0,0,0.08), 0 8px 24px rgba(0,0,0,0.18); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main">
|
||||
{{ range .Pages }}
|
||||
<div class="thumb">
|
||||
<div class="label">{{ .Name }}</div>
|
||||
<iframe src="/{{ .Href }}" loading="lazy"></iframe>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
29
templates/print.gohtml
Normal file
29
templates/print.gohtml
Normal file
@@ -0,0 +1,29 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Print</title>
|
||||
<link rel="stylesheet" href="../css/style.css" />
|
||||
<style>
|
||||
/* Ensure a clean print with no gaps between pages */
|
||||
@media screen { body { background: #eeeeee; } }
|
||||
/* Do NOT constrain the whole document to one page; let it flow across pages */
|
||||
html, body { margin: 0; padding: 0; }
|
||||
@media print {
|
||||
html, body { width: auto !important; height: auto !important; overflow: visible !important; margin: 0 !important; padding: 0 !important; background: white !important; }
|
||||
}
|
||||
/* Each #page is a fixed-size sheet */
|
||||
#page { width: var(--page-w, 5.5in); height: var(--page-h, 8.5in); background: white; box-shadow: none !important; overflow: hidden; }
|
||||
#page { page-break-after: always; break-after: page; }
|
||||
#page:last-child { page-break-after: auto; break-after: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body {{ .PageSizeAttr }}>
|
||||
{{ range .Pages }}
|
||||
<div id="page">{{ .Content }}</div>
|
||||
{{ end }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
32
templates/print_2up.gohtml
Normal file
32
templates/print_2up.gohtml
Normal file
@@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Print 2-Up</title>
|
||||
<link rel="stylesheet" href="../css/style.css" />
|
||||
<style>
|
||||
/* Letter page with two half-letter pages laid out horizontally */
|
||||
/* Do NOT constrain the whole document to a single sheet */
|
||||
html, body { margin: 0; padding: 0; background: white; }
|
||||
@media print { html, body { width: auto !important; height: auto !important; overflow: visible !important; } }
|
||||
|
||||
/* Each .sheet is one letter-sized page */
|
||||
.sheet { display: flex; gap: 0; margin: 0; padding: 0; width: 11in; height: 8.5in; }
|
||||
.cell { width: 5.5in; height: 8.5in; overflow: hidden; }
|
||||
.cell > #page { width: 5.5in; height: 8.5in; box-shadow: none !important; }
|
||||
.sheet { page-break-after: always; break-after: page; }
|
||||
.sheet:last-child { page-break-after: auto; break-after: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{ range .Sheets }}
|
||||
<div class="sheet">
|
||||
<div class="cell"><div id="page">{{ .Left.Content }}</div></div>
|
||||
{{ if .Right }}<div class="cell"><div id="page">{{ .Right.Content }}</div></div>{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user