Compare commits
21 Commits
8c3232c788
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cc464fa9b | |||
| 4c4e79e384 | |||
| ae57353e8f | |||
| fdb4840912 | |||
| 6f4ae177da | |||
| 9b9648d033 | |||
| 4ffc138909 | |||
|
|
08354c8905 | ||
| dc8ab1a0a7 | |||
|
|
4e19b2a1b0 | ||
| 803f07a1b7 | |||
| fd17e511dc | |||
| 29030c573c | |||
| 3fc555844b | |||
| 182ebd14b3 | |||
| da10b32dfa | |||
|
|
d31123f24c | ||
|
|
aac3a7b302 | ||
|
|
cb93752ce7 | ||
|
|
35a7d49a56 | ||
|
|
4aa310c008 |
55
.gitea/workflows/build-pdf.yml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
name: Build PDF
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pdf:
|
||||||
|
name: make pdf
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Go (from go.mod)
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
check-latest: true
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Install Chrome and tools (no snap)
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
if command -v sudo >/dev/null 2>&1; then SUDO=sudo; else SUDO=; fi
|
||||||
|
$SUDO apt-get update
|
||||||
|
# Base tools and fonts
|
||||||
|
DEBIAN_FRONTEND=noninteractive $SUDO apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates gnupg make fonts-liberation
|
||||||
|
|
||||||
|
# Add Google's official Chrome APT repo (avoids Ubuntu's snap-only chromium)
|
||||||
|
$SUDO install -d -m 0755 /etc/apt/keyrings
|
||||||
|
curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | $SUDO gpg --dearmor -o /etc/apt/keyrings/google-linux-signing-keyring.gpg
|
||||||
|
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/google-linux-signing-keyring.gpg] https://dl.google.com/linux/chrome/deb/ stable main" | \
|
||||||
|
$SUDO tee /etc/apt/sources.list.d/google-chrome.list >/dev/null
|
||||||
|
$SUDO apt-get update
|
||||||
|
DEBIAN_FRONTEND=noninteractive $SUDO apt-get install -y --no-install-recommends google-chrome-stable
|
||||||
|
|
||||||
|
# Clean up apt lists to keep image lean
|
||||||
|
$SUDO rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
- name: Build PDF
|
||||||
|
env:
|
||||||
|
# Ensure our tools pick Chrome first if multiple are present
|
||||||
|
CHROME_PATH: /usr/bin/google-chrome-stable
|
||||||
|
run: make pdf
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: output-pdf
|
||||||
|
path: _dist/output.pdf
|
||||||
|
if-no-files-found: error
|
||||||
4
Makefile
@@ -34,8 +34,8 @@ deps:
|
|||||||
|
|
||||||
.PHONY: pdf
|
.PHONY: pdf
|
||||||
pdf: build deps
|
pdf: build deps
|
||||||
@echo "Generating PDF with headless Chrome..."
|
@echo "Generating PDF by rendering each page and merging..."
|
||||||
@$(GO) run ./cmd/pdf --in $(OUT_DIR)/print.html --out $(PDF)
|
@$(GO) run ./cmd/pdfbook --pages $(OUT_DIR) --order pages.yaml --out $(PDF)
|
||||||
|
|
||||||
.PHONY: pdf-2up
|
.PHONY: pdf-2up
|
||||||
pdf-2up: build deps
|
pdf-2up: build deps
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Heading Now";
|
||||||
|
src: url("../font/HeadingNow-95Medium.otf");
|
||||||
|
}
|
||||||
|
|
||||||
/* Base */
|
/* Base */
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -20,6 +25,39 @@ html, body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.interlude {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
color: white;
|
||||||
|
padding: 4rem;
|
||||||
|
z-index: 0; /* ensure stacking context */
|
||||||
|
}
|
||||||
|
|
||||||
|
.interlude::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
filter: grayscale(20%) brightness(0.7);
|
||||||
|
z-index: -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interlude::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
/*background: rgba(128, 0, 128, 0.3); /* purple tint */ */
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interlude-caption {
|
||||||
|
color: rgba(225, 225, 225, 1.0);
|
||||||
|
font-family: "Heading Now";
|
||||||
|
line-height: 2em;
|
||||||
|
rotate: 5deg;
|
||||||
|
}
|
||||||
|
|
||||||
/* Use border-box sizing everywhere to keep dimensions predictable */
|
/* Use border-box sizing everywhere to keep dimensions predictable */
|
||||||
html { box-sizing: border-box; }
|
html { box-sizing: border-box; }
|
||||||
*, *::before, *::after { box-sizing: inherit; }
|
*, *::before, *::after { box-sizing: inherit; }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Heading Now";
|
font-family: "Heading Now";
|
||||||
src: url("/assets/font/HeadingNow-95Medium.otf");
|
src: url("../font/HeadingNow-95Medium.otf");
|
||||||
}
|
}
|
||||||
|
|
||||||
.ytmnd-1 {
|
.ytmnd-1 {
|
||||||
|
|||||||
BIN
assets/font/Chalk-Regular.ttf
Normal file
BIN
assets/img/broken-screen.jpg
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
assets/img/coffee-shop.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
assets/img/crashman/crashman-1.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
assets/img/crashman/crashman-2.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
assets/img/crashman/crashman-3.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
assets/img/crashman/crashman-bg.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
assets/img/free-dirt.jpg
Normal file
|
After Width: | Height: | Size: 5.8 MiB |
BIN
assets/img/hack-the-planet.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/img/javaguy.jpg
Normal file
|
After Width: | Height: | Size: 993 KiB |
BIN
assets/img/mr-nickel.jpg
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
assets/img/vrankle.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
assets/img/windows-shirt.jpg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
assets/img/wtf.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
@@ -8,7 +8,6 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -27,152 +26,6 @@ func floatToIn(v float64) string {
|
|||||||
return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.4fin", v), "0"), ".")
|
return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.4fin", v), "0"), ".")
|
||||||
}
|
}
|
||||||
|
|
||||||
func min(a, b int) int {
|
|
||||||
if a < b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractAndScopeCSS extracts <style> blocks from HTML content and scopes them to a page-specific selector
|
|
||||||
func extractAndScopeCSS(content string, pageID string) (string, string) {
|
|
||||||
styleRegex := regexp.MustCompile(`(?s)<style[^>]*>(.*?)</style>`)
|
|
||||||
matches := styleRegex.FindAllStringSubmatch(content, -1)
|
|
||||||
|
|
||||||
var scopedCSS strings.Builder
|
|
||||||
contentWithoutCSS := content
|
|
||||||
|
|
||||||
// Remove all <style> blocks from content
|
|
||||||
contentWithoutCSS = styleRegex.ReplaceAllString(contentWithoutCSS, "")
|
|
||||||
|
|
||||||
// Process each CSS block and scope it
|
|
||||||
for _, match := range matches {
|
|
||||||
cssContent := strings.TrimSpace(match[1])
|
|
||||||
if cssContent == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scope the CSS rules to the page
|
|
||||||
scopedCSS.WriteString(scopeCSS(cssContent, pageID))
|
|
||||||
scopedCSS.WriteString("\n\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return contentWithoutCSS, scopedCSS.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// scopeCSS wraps CSS rules with a page-specific selector
|
|
||||||
func scopeCSS(css string, pageID string) string {
|
|
||||||
// Simple CSS scoping - wrap all rules with the page selector
|
|
||||||
lines := strings.Split(css, "\n")
|
|
||||||
var scopedLines []string
|
|
||||||
var inRule bool
|
|
||||||
var ruleBuffer strings.Builder
|
|
||||||
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
|
|
||||||
// Skip comments and empty lines
|
|
||||||
if trimmed == "" || strings.HasPrefix(trimmed, "/*") {
|
|
||||||
scopedLines = append(scopedLines, line)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle @-rules (like @font-face, @media) - don't scope these
|
|
||||||
if strings.HasPrefix(trimmed, "@") {
|
|
||||||
if inRule {
|
|
||||||
// Finish current rule first
|
|
||||||
scopedLines = append(scopedLines, scopeRuleBuffer(ruleBuffer.String(), pageID))
|
|
||||||
ruleBuffer.Reset()
|
|
||||||
inRule = false
|
|
||||||
}
|
|
||||||
scopedLines = append(scopedLines, line)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect start of CSS rule
|
|
||||||
if strings.Contains(trimmed, "{") && !inRule {
|
|
||||||
inRule = true
|
|
||||||
ruleBuffer.WriteString(line)
|
|
||||||
ruleBuffer.WriteString("\n")
|
|
||||||
} else if inRule {
|
|
||||||
ruleBuffer.WriteString(line)
|
|
||||||
ruleBuffer.WriteString("\n")
|
|
||||||
|
|
||||||
// Check if rule ends
|
|
||||||
if strings.Contains(trimmed, "}") {
|
|
||||||
scopedLines = append(scopedLines, scopeRuleBuffer(ruleBuffer.String(), pageID))
|
|
||||||
ruleBuffer.Reset()
|
|
||||||
inRule = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Standalone line - might be a selector
|
|
||||||
if strings.Contains(trimmed, "{") {
|
|
||||||
ruleBuffer.WriteString(line)
|
|
||||||
ruleBuffer.WriteString("\n")
|
|
||||||
inRule = true
|
|
||||||
} else {
|
|
||||||
scopedLines = append(scopedLines, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle any remaining rule
|
|
||||||
if inRule && ruleBuffer.Len() > 0 {
|
|
||||||
scopedLines = append(scopedLines, scopeRuleBuffer(ruleBuffer.String(), pageID))
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(scopedLines, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func scopeRuleBuffer(rule string, pageID string) string {
|
|
||||||
lines := strings.Split(strings.TrimSpace(rule), "\n")
|
|
||||||
if len(lines) == 0 {
|
|
||||||
return rule
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the selector line (first line before {)
|
|
||||||
for i, line := range lines {
|
|
||||||
if strings.Contains(line, "{") {
|
|
||||||
// Extract selector part
|
|
||||||
parts := strings.Split(line, "{")
|
|
||||||
if len(parts) >= 2 {
|
|
||||||
selector := strings.TrimSpace(parts[0])
|
|
||||||
rest := "{" + strings.Join(parts[1:], "{")
|
|
||||||
|
|
||||||
// Scope the selector
|
|
||||||
scopedSelector := scopeSelector(selector, pageID)
|
|
||||||
lines[i] = scopedSelector + " " + rest
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(lines, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func scopeSelector(selector string, pageID string) string {
|
|
||||||
// Split multiple selectors by comma
|
|
||||||
selectors := strings.Split(selector, ",")
|
|
||||||
var scopedSelectors []string
|
|
||||||
|
|
||||||
for _, sel := range selectors {
|
|
||||||
sel = strings.TrimSpace(sel)
|
|
||||||
if sel == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't scope selectors that already include the page ID or are body/html
|
|
||||||
if strings.Contains(sel, "#"+pageID) || sel == "body" || sel == "html" {
|
|
||||||
scopedSelectors = append(scopedSelectors, sel)
|
|
||||||
} else {
|
|
||||||
// Scope to page
|
|
||||||
scopedSelectors = append(scopedSelectors, "#"+pageID+" "+sel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(scopedSelectors, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
type PageData struct {
|
type PageData struct {
|
||||||
Title string
|
Title string
|
||||||
Content template.HTML
|
Content template.HTML
|
||||||
@@ -185,8 +38,6 @@ type IndexPage struct {
|
|||||||
|
|
||||||
type PrintPage struct {
|
type PrintPage struct {
|
||||||
Content template.HTML
|
Content template.HTML
|
||||||
PageID string
|
|
||||||
ScopedCSS template.HTML
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TwoUpSheet struct {
|
type TwoUpSheet struct {
|
||||||
@@ -309,52 +160,11 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build print.html containing all pages one after another for PDF
|
// Prepare pages for aggregate printing (2-up, etc.)
|
||||||
printTplPath := filepath.Join(templatesDir, "print.gohtml")
|
|
||||||
printTpl := template.Must(template.New("print").Parse(mustReadFile(printTplPath)))
|
|
||||||
var printPages []PrintPage
|
var printPages []PrintPage
|
||||||
var allScopedCSS strings.Builder
|
|
||||||
|
|
||||||
for _, pagePath := range pageFiles {
|
for _, pagePath := range pageFiles {
|
||||||
content := strings.TrimSpace(mustReadFile(pagePath))
|
content := strings.TrimSpace(mustReadFile(pagePath))
|
||||||
|
printPages = append(printPages, PrintPage{Content: template.HTML(content)})
|
||||||
// Generate unique page ID based on filename
|
|
||||||
base := filepath.Base(pagePath)
|
|
||||||
pageID := strings.TrimSuffix(base, filepath.Ext(base)) + "-page"
|
|
||||||
|
|
||||||
// Extract and scope CSS
|
|
||||||
contentWithoutCSS, scopedCSS := extractAndScopeCSS(content, pageID)
|
|
||||||
|
|
||||||
// Collect all scoped CSS
|
|
||||||
if scopedCSS != "" {
|
|
||||||
allScopedCSS.WriteString(scopedCSS)
|
|
||||||
allScopedCSS.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
printPages = append(printPages, PrintPage{
|
|
||||||
Content: template.HTML(contentWithoutCSS),
|
|
||||||
PageID: pageID,
|
|
||||||
ScopedCSS: template.HTML(scopedCSS),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var outPrint bytes.Buffer
|
|
||||||
if err := printTpl.Execute(&outPrint, map[string]any{
|
|
||||||
"Pages": printPages,
|
|
||||||
"PageSizeAttr": pageSizeStyleAttr(config.PageWidthIn, config.PageHeightIn),
|
|
||||||
}); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject scoped CSS into the output
|
|
||||||
cssToInject := allScopedCSS.String()
|
|
||||||
finalOutput := strings.ReplaceAll(
|
|
||||||
outPrint.String(),
|
|
||||||
"PLACEHOLDER_FOR_SCOPED_CSS",
|
|
||||||
cssToInject,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := writeFile(filepath.Join(outDir, "print.html"), []byte(finalOutput)); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build 2-up print HTML (letter pages with two half-letter pages side by side)
|
// Build 2-up print HTML (letter pages with two half-letter pages side by side)
|
||||||
@@ -371,20 +181,10 @@ func main() {
|
|||||||
print2TplPath := filepath.Join(templatesDir, "print_2up.gohtml")
|
print2TplPath := filepath.Join(templatesDir, "print_2up.gohtml")
|
||||||
print2Tpl := template.Must(template.New("print2up").Parse(mustReadFile(print2TplPath)))
|
print2Tpl := template.Must(template.New("print2up").Parse(mustReadFile(print2TplPath)))
|
||||||
var outPrint2 bytes.Buffer
|
var outPrint2 bytes.Buffer
|
||||||
if err := print2Tpl.Execute(&outPrint2, map[string]any{
|
if err := print2Tpl.Execute(&outPrint2, map[string]any{"Sheets": sheets}); err != nil {
|
||||||
"Sheets": sheets,
|
|
||||||
}); err != nil {
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
if err := writeFile(filepath.Join(outDir, "print_2up.html"), outPrint2.Bytes()); err != nil {
|
||||||
// Inject scoped CSS into the 2-up output
|
|
||||||
finalOutput2Up := strings.ReplaceAll(
|
|
||||||
outPrint2.String(),
|
|
||||||
"PLACEHOLDER_FOR_SCOPED_CSS",
|
|
||||||
cssToInject,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := writeFile(filepath.Join(outDir, "print_2up.html"), []byte(finalOutput2Up)); err != nil {
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
158
cmd/pdfbook/main.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"smartbar/internal/config"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto/emulation"
|
||||||
|
"github.com/chromedp/cdproto/page"
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
pdfapi "github.com/pdfcpu/pdfcpu/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func must(err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseYAMLListOfStrings(src string) ([]string, error) {
|
||||||
|
var out []string
|
||||||
|
for _, line := range strings.Split(src, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "-") {
|
||||||
|
item := strings.TrimSpace(strings.TrimPrefix(line, "-"))
|
||||||
|
if item != "" {
|
||||||
|
out = append(out, item)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("invalid yaml line (expect '- item'): %s", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findChromeExec() (string, error) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// macOS common app bundle paths
|
||||||
|
macPaths := []string{
|
||||||
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||||
|
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
||||||
|
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||||
|
}
|
||||||
|
for _, p := range macPaths {
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no Chrome/Chromium executable found; set CHROME_PATH or install chromium/google-chrome")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
pagesDir string
|
||||||
|
orderPath string
|
||||||
|
outPDF string
|
||||||
|
width float64
|
||||||
|
height float64
|
||||||
|
)
|
||||||
|
flag.StringVar(&pagesDir, "pages", "_dist", "path to compiled pages directory (required)")
|
||||||
|
flag.StringVar(&orderPath, "order", "pages.yaml", "path to YAML file listing page order (required)")
|
||||||
|
flag.StringVar(&outPDF, "out", "_dist/output.pdf", "output PDF path (required)")
|
||||||
|
flag.Float64Var(&width, "w", config.PageWidthIn, "page width in inches")
|
||||||
|
flag.Float64Var(&height, "h", config.PageHeightIn, "page height in inches")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if pagesDir == "" || orderPath == "" || outPDF == "" {
|
||||||
|
log.Fatal("--pages, --order and --out are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load order
|
||||||
|
data, err := os.ReadFile(orderPath)
|
||||||
|
must(err)
|
||||||
|
|
||||||
|
ordered, err := parseYAMLListOfStrings(string(data))
|
||||||
|
must(err)
|
||||||
|
|
||||||
|
// Resolve Chrome
|
||||||
|
chromeExec, err := findChromeExec()
|
||||||
|
must(err)
|
||||||
|
|
||||||
|
opts := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.ExecPath(chromeExec), 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()
|
||||||
|
|
||||||
|
// Temp dir for individual PDFs
|
||||||
|
tmpDir, err := os.MkdirTemp("", "smartbar_pdfs_*")
|
||||||
|
must(err)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
var partPDFs []string
|
||||||
|
for _, rel := range ordered {
|
||||||
|
abs := filepath.Join(pagesDir, rel)
|
||||||
|
if _, err := os.Stat(abs); err != nil {
|
||||||
|
log.Printf("skip missing page: %s", rel)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
absHTML, _ := filepath.Abs(abs)
|
||||||
|
url := "file://" + absHTML
|
||||||
|
|
||||||
|
outPath := filepath.Join(tmpDir, strings.TrimSuffix(filepath.Base(rel), filepath.Ext(rel)) + ".pdf")
|
||||||
|
// Render one page PDF via printToPDF
|
||||||
|
err := chromedp.Run(ctx, chromedp.Tasks{
|
||||||
|
chromedp.Navigate(url),
|
||||||
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
|
if err := emulation.SetEmulatedMedia().WithMedia("print").Do(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
params := page.PrintToPDF().
|
||||||
|
WithPaperWidth(width).
|
||||||
|
WithPaperHeight(height).
|
||||||
|
WithMarginTop(0).WithMarginBottom(0).WithMarginLeft(0).WithMarginRight(0).
|
||||||
|
WithPrintBackground(true)
|
||||||
|
buf, _, err := params.Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(outPath, buf, 0o644)
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
must(err)
|
||||||
|
partPDFs = append(partPDFs, outPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(partPDFs) == 0 {
|
||||||
|
log.Fatal("no pages produced; check order file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge PDFs into single output
|
||||||
|
must(os.MkdirAll(filepath.Dir(outPDF), 0o755))
|
||||||
|
|
||||||
|
// pdfcpu expects []string in order and writes outPDF
|
||||||
|
must(pdfapi.MergeCreateFile(partPDFs, outPDF, false, nil))
|
||||||
|
fmt.Printf("Wrote %s (%d pages)\n", outPDF, len(partPDFs))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
17
go.mod
@@ -1,11 +1,14 @@
|
|||||||
module smartbar
|
module smartbar
|
||||||
|
|
||||||
go 1.21
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732
|
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732
|
||||||
github.com/chromedp/chromedp v0.9.5
|
github.com/chromedp/chromedp v0.9.5
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
|
github.com/pdfcpu/pdfcpu v0.11.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -13,7 +16,17 @@ require (
|
|||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/gobwas/ws v1.3.2 // indirect
|
github.com/gobwas/ws v1.3.2 // indirect
|
||||||
|
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||||
|
github.com/hhrutter/pkcs7 v0.2.0 // indirect
|
||||||
|
github.com/hhrutter/tiff v1.0.2 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
golang.org/x/sys v0.16.0 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
golang.org/x/crypto v0.38.0 // indirect
|
||||||
|
golang.org/x/image v0.27.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
golang.org/x/text v0.25.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
28
go.sum
@@ -12,14 +12,40 @@ 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/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 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
|
||||||
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
|
github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0=
|
||||||
|
github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo=
|
||||||
|
github.com/hhrutter/pkcs7 v0.2.0 h1:i4HN2XMbGQpZRnKBLsUwO3dSckzgX142TNqY/KfXg+I=
|
||||||
|
github.com/hhrutter/pkcs7 v0.2.0/go.mod h1:aEzKz0+ZAlz7YaEMY47jDHL14hVWD6iXt0AgqgAvWgE=
|
||||||
|
github.com/hhrutter/tiff v1.0.2 h1:7H3FQQpKu/i5WaSChoD1nnJbGx4MxU5TlNqqpxw55z8=
|
||||||
|
github.com/hhrutter/tiff v1.0.2/go.mod h1:pcOeuK5loFUE7Y/WnzGw20YxUdnqjY1P0Jlcieb/cCw=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
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/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 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
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 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
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=
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
|
github.com/pdfcpu/pdfcpu v0.11.0 h1:mL18Y3hSHzSezmnrzA21TqlayBOXuAx7BUzzZyroLGM=
|
||||||
|
github.com/pdfcpu/pdfcpu v0.11.0/go.mod h1:F1ca4GIVFdPtmgvIdvXAycAm88noyNxZwzr9CpTy+Mw=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||||
|
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||||
|
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
|
||||||
|
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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=
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
|||||||
12
pages.yaml
@@ -1,9 +1,19 @@
|
|||||||
# locations in pages/
|
# locations in pages/
|
||||||
|
|
||||||
- cover.html
|
- cover.html
|
||||||
|
- hack-the-planet.html
|
||||||
|
- wtf.html
|
||||||
|
- drivingmissmuni.html
|
||||||
|
- crashman.html
|
||||||
|
- vranklevictim.html
|
||||||
|
- pearlstreetcafe.html
|
||||||
- gtkapplang-1.html
|
- gtkapplang-1.html
|
||||||
- gtkapplang-2.html
|
- gtkapplang-2.html
|
||||||
- gtkapplang-3.html
|
- gtkapplang-3.html
|
||||||
- gtkapplang-4.html
|
- gtkapplang-4.html
|
||||||
- drivingmissmuni.html
|
- broken-screen.html
|
||||||
|
- windows-shirt.html
|
||||||
|
- free-dirt.html
|
||||||
|
- mr-nickel.html
|
||||||
|
- javaguy.html
|
||||||
|
|
||||||
|
|||||||
19
pages/broken-screen.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<style>
|
||||||
|
#broken-screen::before {
|
||||||
|
background-image: url("assets/img/broken-screen.jpg");
|
||||||
|
}
|
||||||
|
|
||||||
|
#broken-screen-caption {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 25%;
|
||||||
|
right: 15%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="broken-screen" class="page-base interlude">
|
||||||
|
<div class="interlude-caption" id="broken-screen-caption">
|
||||||
|
have you tried<br/>
|
||||||
|
turning it off and on again?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
background:
|
background:
|
||||||
linear-gradient(rgba(0,0,0,0.35), rgba(0,0,0,0.35)),
|
linear-gradient(rgba(0,0,0,0.35), rgba(0,0,0,0.35)),
|
||||||
url("/assets/img/cover.jpg") no-repeat center center;
|
url("assets/img/cover.jpg") no-repeat center center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
89
pages/crashman.html
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<style>
|
||||||
|
#crashman-page {
|
||||||
|
position: relative;
|
||||||
|
background: url("assets/img/crashman/crashman-bg.png") no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#snips {
|
||||||
|
height: 120px;
|
||||||
|
width: 620px;
|
||||||
|
background-color: rgb(209, 1, 209);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
bottom: 30px;
|
||||||
|
left: -50px;
|
||||||
|
transform: rotate(-5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#snips img {
|
||||||
|
flex: 1 1 0;
|
||||||
|
width: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
|
position: absolute;
|
||||||
|
top: 350px;
|
||||||
|
right: 8px;
|
||||||
|
|
||||||
|
font-family: "Heading Now";
|
||||||
|
font-size: 48px;
|
||||||
|
|
||||||
|
color: #fff;
|
||||||
|
text-shadow:
|
||||||
|
0px 10px 0 rgb(209, 1, 209),
|
||||||
|
0px 20px 0 black;
|
||||||
|
}
|
||||||
|
|
||||||
|
#text {
|
||||||
|
position: absolute;
|
||||||
|
top: 390px;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
background-color: rgba(62, 6, 62, 0.704);
|
||||||
|
font: 11.75pt/1.40 Tahoma, sans-serif;
|
||||||
|
padding: 24px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stretch {
|
||||||
|
font-weight: 800;
|
||||||
|
transform: scaleX(2.1);
|
||||||
|
transform-origin: left;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="crashman-page" class="page-base">
|
||||||
|
|
||||||
|
<div id="text">
|
||||||
|
<p>
|
||||||
|
<b>Rusty Haight</b> is the human crash-test dummy. As director of the San Diego-based Collision Safety Institute,
|
||||||
|
Rusty has experienced more than 950 violent vehicle crash tests at speeds of up to 54 mph,
|
||||||
|
and taken <span class="stretch">140 air bags to the face.</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Haight has contributed several publications on the topic of automobile safety
|
||||||
|
in <b>Collision Magazine</b> such as <em>Hyundai and Kia Crash Data: the Indispensable Compendium.</em>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="snips">
|
||||||
|
<img src="assets/img/crashman/crashman-1.png" />
|
||||||
|
<img src="assets/img/crashman/crashman-2.png" />
|
||||||
|
<img src="assets/img/crashman/crashman-3.png" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="title" class="ytmnd-1">crash man</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
15
pages/free-dirt.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<style>
|
||||||
|
#free-dirt::before {
|
||||||
|
background-image: url("assets/img/free-dirt.jpg");
|
||||||
|
}
|
||||||
|
|
||||||
|
#free-dirt-caption {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 25%;
|
||||||
|
right: 25%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="free-dirt" class="page-base interlude">
|
||||||
|
<div class="interlude-caption" id="free-dirt-caption">thanks...</div>
|
||||||
|
</div>
|
||||||
10
pages/hack-the-planet.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<style>
|
||||||
|
#hack-the-planet-page {
|
||||||
|
background: url("assets/img/hack-the-planet.jpg") no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="hack-the-planet-page" class="page-base">
|
||||||
|
</div>
|
||||||
18
pages/javaguy.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<style>
|
||||||
|
#javaguy::before {
|
||||||
|
background-image: url("assets/img/javaguy.jpg");
|
||||||
|
}
|
||||||
|
|
||||||
|
#javaguy-caption {
|
||||||
|
position: absolute;
|
||||||
|
top: 5%;
|
||||||
|
left: 5%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="javaguy" class="page-base interlude">
|
||||||
|
<div class="interlude-caption" id="javaguy-caption">
|
||||||
|
i'm living in the real world<br/>
|
||||||
|
and he's living in a <em>virtual machine...</em><br/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
19
pages/mr-nickel.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<style>
|
||||||
|
#mr-nickel::before {
|
||||||
|
background-image: url("assets/img/mr-nickel.jpg");
|
||||||
|
}
|
||||||
|
|
||||||
|
#mr-nickel-caption {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10%;
|
||||||
|
right: 5%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="mr-nickel" class="page-base interlude">
|
||||||
|
<div class="interlude-caption" id="mr-nickel-caption">
|
||||||
|
mr. nickel says<br/>
|
||||||
|
the financial collapse is imminent<br/>
|
||||||
|
and it's all your fault!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
85
pages/pearlstreetcafe.html
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<style>
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Chalk";
|
||||||
|
src: url("assets/font/Chalk-Regular.ttf");
|
||||||
|
}
|
||||||
|
|
||||||
|
div#body {
|
||||||
|
columns: 3;
|
||||||
|
font: 8.4pt 'Times New Roman', serif;
|
||||||
|
padding: 0.5in;
|
||||||
|
background-image: url("assets/img/coffee-shop.jpg");
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
background-blend-mode: overlay;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
background-position-x: -250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-cap::first-letter {
|
||||||
|
float: left;
|
||||||
|
font-size: 4em;
|
||||||
|
line-height: 0.8;
|
||||||
|
padding-right: 8px;
|
||||||
|
padding-top: 0px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: Georgia, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-cap {
|
||||||
|
text-indent: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2 {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: Chalk, Helvetica, sans-serif;
|
||||||
|
font-size: xx-large;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: small;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: normal;
|
||||||
|
color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
#body p {
|
||||||
|
color: white;
|
||||||
|
text-indent: 2em;
|
||||||
|
text-align: justify;
|
||||||
|
text-justify: inter-word;
|
||||||
|
hyphens: auto;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="body" class="page-base">
|
||||||
|
|
||||||
|
<h1>Pearl Street Cafe</h1>
|
||||||
|
<h2>Short Story by <strong>Bram Noidz</strong></h2>
|
||||||
|
|
||||||
|
<p class="drop-cap">Trevor woke up at the usual time, naturally, without an alarm clock. A lot of podcasts are discussing the topic of mental health, and apparently waking up with an alarm is bad for anxiety. It took a couple of weeks but Trevor finally tuned his circadian rhythms to obey his schedule, rather than the other way around. He flops out of bed.</p>
|
||||||
|
|
||||||
|
<p>The brain fog was especially thick this morning. Trevor went through the process of malaise attribution. Perhaps it was because of the six o'clock coffee the evening before? Or could it be work-induced stress? He had just recovered from a bout of illness due to the latest strain of respiratory viruses circulating around. Maybe that was it. Nothing the cold plunge can't thaw. Trevor prepared the ice bath while contemplating whether the social stigma around caffeine addiction is morally justified.</p>
|
||||||
|
|
||||||
|
<p>With the morning routine out of the way, it's time to grind. Work must follow every morning routine, otherwise there is no point to the routine in the first place.</p>
|
||||||
|
|
||||||
|
<p>Trevor grabbed his 15-inch MacBook Pro from the nightstand and stuffed it into his messenger bag. He boarded his self-driving SUV and hitched a ride by himself to the local coffee shop. Pearl Street has about three or four coffee shops that are worth the time spent indoors, and several others that are not. Only two of them have WiFi that is reliable enough for Laptop Work. And out of those two, only one of them actually has coffee that tastes beanworthy. <em>Navigation complete.</em></p>
|
||||||
|
|
||||||
|
<p>The barista got to work on Trevor's double shot, low foam latté. He takes a seat in the corner of the café where the best reception is available. It always takes a few minutes after opening up the laptop before Trevor remembers what his job actually is. Something with numbers. <em>A transponster?</em> Colleagues whom he's never actually met had sent messages during his cold plunge and while he was sleeping soundly, and reading them allows the work gets context switched back into local memory. Sometimes he wonders if <em>Ms. Trish</em> and <em>Mr. Herb</em> are actually North Korean remote workers, scamming fiat to fund the regime. He realizes that he doesn't care.</p>
|
||||||
|
|
||||||
|
<p>Two hours fly by in an instant. Almost time for lunch. Usually it is only the Numbers and lunch that occupy Trevor's mind at this time of the day. But this time he was feeling pensive for some reason. Mom once asked Trevor what he actually did at his job. She worked in a grocery store with her hands so she wanted to know the concrete details about what he did during the day that let him put food on the table. Ultimately it just came down to typing and clicking on a computer. That's it? Someone's paying for it so it must be worth something.</p>
|
||||||
|
|
||||||
|
<p>Trevor was halfway through eating his <em>Spam Sandwich</em> when everything came crashing down like a house of cards. Production was not even remotely the point of his job. A modern economy functions much like an electronic circuit, where electrons move from one point of high electric potential towards another point with lower electric potential. Without this difference in potential between two points, the circuit is inert and useless. If Trevor wasn't welding steel beams or fixing toilets, then he must be located in the opposite polarity. <em>An electron sink. A ground prong. A consumer.</em></p>
|
||||||
|
|
||||||
|
<p>So what, then, is the point of doing the Numbers? Sending messages to <em>Ms. Trish</em> and <em>Mr. Herb</em>? It's to justify Trevor's consumption. Consumption without work, no matter how fake the work is, is not sustainable after millions of years of cultural evolution that put selective pressure on becoming a productive member of society. Trevor's job is not the Numbers. It's the Spam Sandwich.</p>
|
||||||
|
|
||||||
|
<p>Terror turns into loathing, and loathing turns into acceptance. Tyler Durden took a different path halfway through this revelation, but Trevor's feels more peaceful and more righteous. Wonder what's on TV tonight. <span class="endmark">■</span></p>
|
||||||
|
|
||||||
|
</div>
|
||||||
148
pages/vranklevictim.html
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
width: 5.5in;
|
||||||
|
height: 8.5in;
|
||||||
|
position: relative;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,h2,h3 {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 4rem;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: "Impact";
|
||||||
|
color: darkred;
|
||||||
|
rotate: 5deg;
|
||||||
|
border: solid;
|
||||||
|
border-width: 0.125in;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-left: 0.25in;
|
||||||
|
margin-right: 0.25in;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
margin-left: 0.25in;
|
||||||
|
margin-right: 0.25in;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
grey 0%,
|
||||||
|
black 45%,
|
||||||
|
black, black
|
||||||
|
);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div * {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-top: 2.85in;
|
||||||
|
width: 50%;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
rotate: -10deg;
|
||||||
|
scale: 115%;
|
||||||
|
filter: saturate(0.45) sepia(0.8) brightness(0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
aside {
|
||||||
|
width: 100%;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: center;
|
||||||
|
margin-left: 0.25in;
|
||||||
|
margin-right: 0.25in;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
text-transform: uppercase;
|
||||||
|
background-image: radial-gradient(yellow, goldenrod);
|
||||||
|
color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.byline {
|
||||||
|
background-color: #F0E0D6;
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<aside style="position: absolute; top: 0;">
|
||||||
|
Coming exclusively to <span class="logo">ULTRA-PLUS®</span> on Christmas Day
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<figure style="height: 0.3in"></figure>
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
Vrankle: A Victim
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<img src="assets/img/vrankle.png"/>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
The true story of an innocent man's struggle
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Jer Vrankle was attending a fun demonstration downtown when a brute ignorantly called his baseball cap "cool" in the co-op grocer's parking lot. Vrankle was thrust into crisis. Recovering from a small cold and running a mild fever, Jer was the opposite of "cool". The property security dismissed complaints about the comment.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<figure style="height: 2.5in;"></figure>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The 48 episode docuseries, directed by the award-winning Selgus Bango, deals with the complex struggles plaguing society today: relationships, decency, and verbal communication.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Learn about every aspect of the conflict. Make up your own mind about the innocent Jer and antagonistic brute. Hear interviews with bystanders parking in the lot, watch the security camera footage of the event, and listen to Ivy League professors unpack every detail.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Famed composer Hilden Valeigh's soundtrack is a complex mix of droning repetitive ambiance. The 9 disc soundtrack is releasing exclusively for streaming on <span class="logo">ULTRA-PLUS®</span> later this winter.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<aside style="position: absolute; bottom: 32px;">
|
||||||
|
Watch the 48 episode docuseries on <span class="logo">ULTRA-PLUS®</span>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="byline">
|
||||||
|
<span style="color: black;">by</span>
|
||||||
|
<span style="color: black;">[+]</span>
|
||||||
|
<span style="font-weight: bold; color: forestgreen;">Max Res Default</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
19
pages/windows-shirt.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<style>
|
||||||
|
#windows-shirt::before {
|
||||||
|
background-image: url("assets/img/windows-shirt.jpg");
|
||||||
|
}
|
||||||
|
|
||||||
|
#windows-shirt-caption {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10%;
|
||||||
|
left: 10%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="windows-shirt" class="page-base interlude">
|
||||||
|
<div class="interlude-caption" id="windows-shirt-caption">
|
||||||
|
we line up to store<br/>
|
||||||
|
only 4 gigabytes allowed<br/>
|
||||||
|
comrade, be grateful!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
136
pages/wtf.html
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<style>
|
||||||
|
#wtf-page {
|
||||||
|
background: url("assets/img/wtf.png") no-repeat center center;
|
||||||
|
background-size: cover;
|
||||||
|
|
||||||
|
font: 8.75pt/1.40 Tahoma, sans-serif;
|
||||||
|
padding: 10px;
|
||||||
|
hyphens: auto;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-family: "Heading Now";
|
||||||
|
font-size: 23px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-family: "Heading Now";
|
||||||
|
font-size: 24px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
font-size: 12.75pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Windows 98 styles */
|
||||||
|
.win98-window {
|
||||||
|
background: #c0c0c0c0;
|
||||||
|
border: 2px solid;
|
||||||
|
border-color: #ffffff #404040 #404040 #ffffff; /* light top/left, dark bottom/right */
|
||||||
|
padding: 6px;
|
||||||
|
|
||||||
|
/* Vertically center the window in the page */
|
||||||
|
position: relative;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.win98-titlebar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
min-height: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(90deg, #000080 0%, #1084d0 100%);
|
||||||
|
border: 2px solid;
|
||||||
|
border-color: #ffffff #404040 #404040 #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.win98-content {
|
||||||
|
background: #ffffffc0;
|
||||||
|
border: 2px solid;
|
||||||
|
border-color: #808080 #ffffff #ffffff #808080; /* inset look */
|
||||||
|
padding: 10px;
|
||||||
|
color: #000000;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feces {
|
||||||
|
transform: skewX(-10deg);
|
||||||
|
transform-origin: bottom center;
|
||||||
|
display: inline-block;
|
||||||
|
color: rgb(154, 77, 0);
|
||||||
|
font-stretch: condensed;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 90%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.round-boxes {
|
||||||
|
display: inline-block;
|
||||||
|
font-stretch: condensed;
|
||||||
|
font-family: inherit;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
background: rgb(166, 205, 211);
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.real {
|
||||||
|
display: inline-block;
|
||||||
|
transform: scaleX(2.5);
|
||||||
|
transform-origin: left;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="wtf-page" class="page-base">
|
||||||
|
<div class="win98-window">
|
||||||
|
<h1 class="win98-titlebar">wtf??? is this???</h1>
|
||||||
|
<div id="wtf-text" class="win98-content">
|
||||||
|
<h2>AH, THE CITY BY THE BAY,</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
If you ask me, I say the city needs some more grime. I
|
||||||
|
know that’s probably not what you were thinking when
|
||||||
|
you stepped over the <span class="feces">HUMAN FECES</span>
|
||||||
|
this morning on the
|
||||||
|
way from picking up your $6 latte. And for damn sure it
|
||||||
|
wasn’t what your UX designer or whatever he calls himself
|
||||||
|
was thinking when he was drawing his stupid fucking
|
||||||
|
<span class="round-boxes">ROUND BOXES</span> for the hundreth time.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Put the round boxes on ice. This is SMART BAR. The
|
||||||
|
“mission district before it was cool” publication. The
|
||||||
|
“noisebridge before they got kicked out” edition.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
You see, here at SMART BAR we don’t give a shit. But we
|
||||||
|
also care. No, not about you. We have our own agenda.
|
||||||
|
What is our agenda? That’s our business.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
SMART BAR is the little bit o’ dirt that makes virtual
|
||||||
|
reality feel more <span class="real">real</span>. It’s the disgusting mess of
|
||||||
|
cables behind your desk that you’re constantly trying
|
||||||
|
to hide. It’s the malware with the pretty UI that tries to
|
||||||
|
steal people’s bitcoin.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>Log on, if you dare.</p>
|
||||||
|
|
||||||
|
<h3>TRENT SNEEK</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Print</title>
|
<title>Print</title>
|
||||||
<link rel="stylesheet" href="assets/css/base.css" />
|
<link rel="stylesheet" href="assets/css/base.css" />
|
||||||
|
<link rel="stylesheet" href="assets/css/smartbar.css" />
|
||||||
<style>
|
<style>
|
||||||
/* Ensure a clean print with no gaps between pages */
|
/* Ensure a clean print with no gaps between pages */
|
||||||
@media screen { body { background: #eeeeee; } }
|
@media screen { body { background: #eeeeee; } }
|
||||||
@@ -13,17 +14,15 @@
|
|||||||
@media print {
|
@media print {
|
||||||
html, body { width: auto !important; height: auto !important; overflow: visible !important; margin: 0 !important; padding: 0 !important; background: white !important; }
|
html, body { width: auto !important; height: auto !important; overflow: visible !important; margin: 0 !important; padding: 0 !important; background: white !important; }
|
||||||
}
|
}
|
||||||
/* Each .page-container is a fixed-size sheet */
|
/* Each #page is a fixed-size sheet */
|
||||||
.page-container { width: var(--page-w, 5.5in); height: var(--page-h, 8.5in); background: white; box-shadow: none !important; overflow: hidden; }
|
#page { width: var(--page-w, 5.5in); height: var(--page-h, 8.5in); background: white; box-shadow: none !important; overflow: hidden; }
|
||||||
.page-container { page-break-after: always; break-after: page; }
|
#page { page-break-after: always; break-after: page; }
|
||||||
.page-container:last-child { page-break-after: auto; break-after: auto; }
|
#page:last-child { page-break-after: auto; break-after: auto; }
|
||||||
|
|
||||||
PLACEHOLDER_FOR_SCOPED_CSS
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body {{ .PageSizeAttr }}>
|
<body {{ .PageSizeAttr }}>
|
||||||
{{ range .Pages }}
|
{{ range .Pages }}
|
||||||
<div class="page-container" id="{{ .PageID }}">{{ .Content }}</div>
|
<div id="page">{{ .Content }}</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -14,18 +14,16 @@
|
|||||||
/* Each .sheet is one letter-sized page */
|
/* Each .sheet is one letter-sized page */
|
||||||
.sheet { display: flex; gap: 0; margin: 0; padding: 0; width: 11in; height: 8.5in; }
|
.sheet { display: flex; gap: 0; margin: 0; padding: 0; width: 11in; height: 8.5in; }
|
||||||
.cell { width: 5.5in; height: 8.5in; overflow: hidden; }
|
.cell { width: 5.5in; height: 8.5in; overflow: hidden; }
|
||||||
.cell > .page-content { width: 5.5in; height: 8.5in; box-shadow: none !important; }
|
.cell > #page { width: 5.5in; height: 8.5in; box-shadow: none !important; }
|
||||||
.sheet { page-break-after: always; break-after: page; }
|
.sheet { page-break-after: always; break-after: page; }
|
||||||
.sheet:last-child { page-break-after: auto; break-after: auto; }
|
.sheet:last-child { page-break-after: auto; break-after: auto; }
|
||||||
|
|
||||||
PLACEHOLDER_FOR_SCOPED_CSS
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{ range .Sheets }}
|
{{ range .Sheets }}
|
||||||
<div class="sheet">
|
<div class="sheet">
|
||||||
<div class="cell"><div class="page-content" id="{{ .Left.PageID }}">{{ .Left.Content }}</div></div>
|
<div class="cell"><div id="page">{{ .Left.Content }}</div></div>
|
||||||
{{ if .Right }}<div class="cell"><div class="page-content" id="{{ .Right.PageID }}">{{ .Right.Content }}</div></div>{{ end }}
|
{{ if .Right }}<div class="cell"><div id="page">{{ .Right.Content }}</div></div>{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||