4 Commits

Author SHA1 Message Date
Charles Magahern
d31123f24c pearlstreetcafe: Formatting tweaks 2025-08-31 15:05:56 -07:00
Charles Magahern
aac3a7b302 pearlstreetcafe: Beavis -> Trevor 2025-08-31 14:50:52 -07:00
Charles Magahern
cb93752ce7 pearlstreetcafe: Formatting tweaks 2025-08-31 13:50:52 -07:00
Charles Magahern
35a7d49a56 pearlstreetcafe: First draft 2025-08-31 13:44:56 -07:00
14 changed files with 116 additions and 269 deletions

View File

@@ -1,17 +1,3 @@
@font-face {
font-family: "Heading Now";
src: url("/assets/font/HeadingNow-95Medium.otf");
}
#page {
.ytmnd-1 {
color: #fff;
text-shadow:
0px 10px 0 rgb(209, 1, 209),
0px 20px 0 black;
}
.ytmnd-2 {
color: #fff;
text-shadow:
0px 6px 0 rgb(209, 1, 209);
}

Binary file not shown.

Binary file not shown.

BIN
assets/img/coffee-shop.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -8,7 +8,6 @@ import (
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
@@ -27,152 +26,6 @@ func floatToIn(v float64) string {
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 {
Title string
Content template.HTML
@@ -185,8 +38,6 @@ type IndexPage struct {
type PrintPage struct {
Content template.HTML
PageID string
ScopedCSS template.HTML
}
type TwoUpSheet struct {
@@ -313,47 +164,15 @@ func main() {
printTplPath := filepath.Join(templatesDir, "print.gohtml")
printTpl := template.Must(template.New("print").Parse(mustReadFile(printTplPath)))
var printPages []PrintPage
var allScopedCSS strings.Builder
for _, pagePath := range pageFiles {
content := strings.TrimSpace(mustReadFile(pagePath))
// 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),
})
printPages = append(printPages, PrintPage{Content: template.HTML(content)})
}
var outPrint bytes.Buffer
if err := printTpl.Execute(&outPrint, map[string]any{
"Pages": printPages,
"PageSizeAttr": pageSizeStyleAttr(config.PageWidthIn, config.PageHeightIn),
}); err != nil {
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 {
if err := writeFile(filepath.Join(outDir, "print.html"), outPrint.Bytes()); err != nil {
panic(err)
}
@@ -371,20 +190,10 @@ func main() {
print2TplPath := filepath.Join(templatesDir, "print_2up.gohtml")
print2Tpl := template.Must(template.New("print2up").Parse(mustReadFile(print2TplPath)))
var outPrint2 bytes.Buffer
if err := print2Tpl.Execute(&outPrint2, map[string]any{
"Sheets": sheets,
}); err != nil {
if err := print2Tpl.Execute(&outPrint2, map[string]any{"Sheets": sheets}); err != nil {
panic(err)
}
// 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 {
if err := writeFile(filepath.Join(outDir, "print_2up.html"), outPrint2.Bytes()); err != nil {
panic(err)
}
}

View File

@@ -1,9 +1,11 @@
# locations in pages/
- cover.html
- bouba.html
- gtkapplang-1.html
- gtkapplang-2.html
- gtkapplang-3.html
- gtkapplang-4.html
- drivingmissmuni.html
- pearlstreetcafe.html

9
pages/bouba.html Normal file
View File

@@ -0,0 +1,9 @@
<style>
#bouba-page {
background-color: red;
}
</style>
<div id="bouba-page" class="page-base">
This is a test kiki.
</div>

View File

@@ -1,49 +1,9 @@
<style>
#cover-page {
position: relative;
background:
linear-gradient(rgba(0,0,0,0.35), rgba(0,0,0,0.35)),
url("/assets/img/cover.jpg") no-repeat center center;
background-size: cover;
}
#logo {
position: absolute;
top: 29px;
left: 8px;
transform: scaleY(1.85);
font-family: "Heading Now";
font-size: 68px;
color: #fff;
text-shadow:
0px 10px 0 rgb(209, 1, 209),
0px 20px 0 black;
}
#edition {
position: absolute;
right: 8px;
bottom: 20px;
transform: scaleY(0.8);
font-family: "Heading Now";
font-size: 32px;
text-align: right;
background-color: green;
}
</style>
<div id="cover-page" class="page-base">
<div id="logo" class="ytmnd-1">smart bar</div>
<div id="edition" class="ytmnd-2">
<div>sf cyberculture zine</div>
<div>issue 1, 2025</div>
</div>
welcome to smart bar
</div>

View 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>

View File

@@ -1,5 +1,5 @@
<!doctype html>
<html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

View File

@@ -1,5 +1,5 @@
<!doctype html>
<html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

View File

@@ -13,17 +13,15 @@
@media print {
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 */
.page-container { 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-container:last-child { page-break-after: auto; break-after: auto; }
PLACEHOLDER_FOR_SCOPED_CSS
/* 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 class="page-container" id="{{ .PageID }}">{{ .Content }}</div>
<div id="page">{{ .Content }}</div>
{{ end }}
</body>
</html>

View File

@@ -14,18 +14,16 @@
/* 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-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:last-child { page-break-after: auto; break-after: auto; }
PLACEHOLDER_FOR_SCOPED_CSS
</style>
</head>
<body>
{{ range .Sheets }}
<div class="sheet">
<div class="cell"><div class="page-content" id="{{ .Left.PageID }}">{{ .Left.Content }}</div></div>
{{ if .Right }}<div class="cell"><div class="page-content" id="{{ .Right.PageID }}">{{ .Right.Content }}</div></div>{{ end }}
<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>