|
|
|
|
@@ -8,6 +8,7 @@ import (
|
|
|
|
|
"io/fs"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"regexp"
|
|
|
|
|
"sort"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
@@ -26,6 +27,152 @@ 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
|
|
|
|
|
@@ -37,7 +184,9 @@ type IndexPage struct {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type PrintPage struct {
|
|
|
|
|
Content template.HTML
|
|
|
|
|
Content template.HTML
|
|
|
|
|
PageID string
|
|
|
|
|
ScopedCSS template.HTML
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TwoUpSheet struct {
|
|
|
|
|
@@ -164,15 +313,47 @@ 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))
|
|
|
|
|
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 {
|
|
|
|
|
if err := printTpl.Execute(&outPrint, map[string]any{
|
|
|
|
|
"Pages": printPages,
|
|
|
|
|
"PageSizeAttr": pageSizeStyleAttr(config.PageWidthIn, config.PageHeightIn),
|
|
|
|
|
}); err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
if err := writeFile(filepath.Join(outDir, "print.html"), outPrint.Bytes()); err != nil {
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -190,10 +371,20 @@ 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)
|
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|